From 402779e8f9fa2b682a1b72f858df96a3ca1c094a Mon Sep 17 00:00:00 2001 From: lateautumn233 Date: Sat, 30 May 2026 16:43:13 +0800 Subject: [PATCH 001/182] feat(vm): add iptables-based port forwarding support - Add applyForward/removeForward abstract methods to FirewallHelper - Implement DNAT and forward rules in IptablesBackend with proper wildcard host IP handling - Add insert() method to ChainInfo for ordered rule insertion - Integrate VMPortForwarder lifecycle into VMInstance start/stop - Add PortForwardListHandler for IPC port forward queries - Add VMPortForwarder to manage configured port forwards --- .../daemon/ipc/vm/PortForwardListHandler.java | 37 +++ .../network/backend/FirewallHelper.java | 11 + .../network/backend/iptables/ChainInfo.java | 12 + .../backend/iptables/IptablesBackend.java | 72 +++++ .../droidvm/daemon/vm/VMInstance.java | 40 +++ .../droidvm/daemon/vm/VMPortForwarder.java | 301 ++++++++++++++++++ 6 files changed, 473 insertions(+) create mode 100644 app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/PortForwardListHandler.java create mode 100644 app/src/main/java/cn/classfun/droidvm/daemon/vm/VMPortForwarder.java diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/PortForwardListHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/PortForwardListHandler.java new file mode 100644 index 000000000..f07796df8 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/PortForwardListHandler.java @@ -0,0 +1,37 @@ +package cn.classfun.droidvm.daemon.ipc.vm; + +import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; + +import androidx.annotation.NonNull; + +import com.google.auto.service.AutoService; + +import org.json.JSONObject; + +import cn.classfun.droidvm.daemon.server.ClientRequest; +import cn.classfun.droidvm.daemon.server.RequestException; +import cn.classfun.droidvm.daemon.server.RequestHandler; + +@AutoService(RequestHandler.class) +public final class PortForwardListHandler extends RequestHandler { + @NonNull + @Override + public String getName() { + return "vm_port_forward_list"; + } + + @Override + public void handle(@NonNull ClientRequest request) throws Exception { + var params = request.getParams(); + var id = params.optString("vm_id", ""); + if (id.isEmpty()) + throw new RequestException("missing vm_id"); + var vm = request.getContext().getVMs().findById(id); + if (vm == null) + throw new RequestException(fmt("VM %s not found", id)); + var data = new JSONObject(); + data.put("configured", vm.getConfiguredPortForwards()); + data.put("active", vm.getActivePortForwards()); + request.res().put("data", data); + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/FirewallHelper.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/FirewallHelper.java index 81433bd7e..dd2cbad82 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/FirewallHelper.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/FirewallHelper.java @@ -1,5 +1,8 @@ package cn.classfun.droidvm.daemon.network.backend; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import cn.classfun.droidvm.daemon.network.NetworkInstance; @SuppressWarnings({"BooleanMethodIsAlwaysInverted", "unused", "UnusedReturnValue"}) @@ -12,4 +15,12 @@ public abstract class FirewallHelper { public abstract void initNetwork(NetworkInstance inst); public abstract void deinitNetwork(NetworkInstance inst); + + public abstract boolean applyForward( + @NonNull String bridge, @NonNull String guestIp, @NonNull String protocol, + @Nullable String hostIp, int hostPort, int guestPort); + + public abstract boolean removeForward( + @NonNull String bridge, @NonNull String guestIp, @NonNull String protocol, + @Nullable String hostIp, int hostPort, int guestPort); } diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/iptables/ChainInfo.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/iptables/ChainInfo.java index 9e1d3c9ce..239eaa33a 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/iptables/ChainInfo.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/iptables/ChainInfo.java @@ -57,6 +57,18 @@ public boolean append(@NonNull String... args) { return backend.iptables(cmd.toArray(new String[0])); } + public boolean insert(@NonNull String... args) { + var cmd = new ArrayList(); + cmd.add("iptables"); + cmd.add("-t"); + cmd.add(table); + cmd.add("-I"); + cmd.add(chain); + cmd.add("1"); + cmd.addAll(Arrays.asList(args)); + return backend.iptables(cmd.toArray(new String[0])); + } + @SuppressWarnings("unused") public boolean delete(@NonNull String... args) { var cmd = new ArrayList(); diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/iptables/IptablesBackend.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/iptables/IptablesBackend.java index c526aa7df..447c5f66f 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/iptables/IptablesBackend.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/iptables/IptablesBackend.java @@ -1,8 +1,11 @@ package cn.classfun.droidvm.daemon.network.backend.iptables; +import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; + import android.util.Log; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import java.util.ArrayList; import java.util.Arrays; @@ -108,6 +111,75 @@ public void deinitNetwork(NetworkInstance inst) { net.deinit(); } + @Override + public boolean applyForward( + @NonNull String bridge, @NonNull String guestIp, @NonNull String protocol, + @Nullable String hostIp, int hostPort, int guestPort + ) { + var pre = new ChainInfo(this, "nat", null, fmt("droidvm_pre_%s", bridge)); + var fwd = new ChainInfo(this, "filter", null, fmt("droidvm_fwd_%s", bridge)); + boolean ok = pre.append(dnatSpec(protocol, hostIp, hostPort, guestIp, guestPort)); + ok &= fwd.insert(fwdSpec(bridge, guestIp, protocol, guestPort)); + Log.i(TAG, fmt("Apply forward %s %s:%d -> %s:%d on %s (ok=%b)", + protocol, isSpecificHost(hostIp) ? hostIp : "*", + hostPort, guestIp, guestPort, bridge, ok)); + return ok; + } + + @Override + public boolean removeForward( + @NonNull String bridge, @NonNull String guestIp, @NonNull String protocol, + @Nullable String hostIp, int hostPort, int guestPort + ) { + var pre = new ChainInfo(this, "nat", null, fmt("droidvm_pre_%s", bridge)); + var fwd = new ChainInfo(this, "filter", null, fmt("droidvm_fwd_%s", bridge)); + boolean ok = pre.delete(dnatSpec(protocol, hostIp, hostPort, guestIp, guestPort)); + ok &= fwd.delete(fwdSpec(bridge, guestIp, protocol, guestPort)); + Log.i(TAG, fmt("Remove forward %s %s:%d -> %s:%d on %s (ok=%b)", + protocol, isSpecificHost(hostIp) ? hostIp : "*", + hostPort, guestIp, guestPort, bridge, ok)); + return ok; + } + + @NonNull + private static String[] dnatSpec( + @NonNull String protocol, @Nullable String hostIp, int hostPort, + @NonNull String guestIp, int guestPort + ) { + var spec = new ArrayList(); + spec.add("-p"); + spec.add(protocol); + if (isSpecificHost(hostIp)) { + spec.add("-d"); + spec.add(hostIp); + } + spec.add("--dport"); + spec.add(String.valueOf(hostPort)); + spec.add("-j"); + spec.add("DNAT"); + spec.add("--to-destination"); + spec.add(fmt("%s:%d", guestIp, guestPort)); + return spec.toArray(new String[0]); + } + + @NonNull + private static String[] fwdSpec( + @NonNull String bridge, @NonNull String guestIp, + @NonNull String protocol, int guestPort + ) { + return new String[]{ + "-o", bridge, "-d", guestIp, "-p", protocol, + "--dport", String.valueOf(guestPort), "-j", "ACCEPT" + }; + } + + /** A wildcard host_ip (empty / 0.0.0.0[/0] / ::[/0]) must omit -d, or the DNAT never matches. */ + private static boolean isSpecificHost(@Nullable String hostIp) { + return hostIp != null && !hostIp.isEmpty() + && !hostIp.equals("0.0.0.0") && !hostIp.equals("0.0.0.0/0") + && !hostIp.equals("::") && !hostIp.equals("::/0"); + } + private boolean enableIpForward() { Log.i(TAG, "Enabling IPv4 forwarding"); var result = RunUtils.run("echo 1 > /proc/sys/net/ipv4/ip_forward"); diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMInstance.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMInstance.java index e73033e61..c5e62e0b5 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMInstance.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMInstance.java @@ -42,6 +42,7 @@ public final class VMInstance extends VMConfig { private BackendBase backend; private VMBackendInstance backendInstance; private Thread workerThread; + private VMPortForwarder portForwarder; private final VMInstanceStore store; public interface VMEventCallback { @@ -321,6 +322,7 @@ private void runVM() { startReaderThread(vmId, stream); } setState(VMState.RUNNING); + startPortForwarding(); int code = process.waitFor(); for (var stream : inst.streams.values()) { var reader = stream.getReaderThread(); @@ -345,6 +347,7 @@ private void runVM() { } else { Log.i(TAG, fmt("VM %s exited with code %d", getName(), code)); } + stopPortForwarding(); cleanupTap(); inst.cleanup(); setState(VMState.STOPPED); @@ -364,6 +367,43 @@ private void cleanupTap() { } } + private void startPortForwarding() { + try { + portForwarder = new VMPortForwarder(this, store.networkStore); + portForwarder.start(); + } catch (Exception e) { + Log.w(TAG, fmt("Failed to start port forwarding for VM %s", getName()), e); + } + } + + private void stopPortForwarding() { + if (portForwarder == null) return; + try { + portForwarder.stop(); + } catch (Exception e) { + Log.w(TAG, fmt("Failed to stop port forwarding for VM %s", getName()), e); + } + portForwarder = null; + } + + @NonNull + public JSONArray getConfiguredPortForwards() throws JSONException { + var arr = new JSONArray(); + var pf = item.opt("port_forwards", null); + if (pf != null && pf.is(DataItem.Type.ARRAY)) + for (var iter : pf) { + var r = iter.getValue(); + if (r.is(DataItem.Type.OBJECT)) arr.put(r.toJson()); + } + return arr; + } + + @NonNull + public JSONArray getActivePortForwards() { + var pf = portForwarder; + return pf != null ? pf.snapshotApplied() : new JSONArray(); + } + private void startReaderThread(@NonNull String vmId, @NonNull ConsoleStream stream) { var streamName = stream.getName(); if (!stream.isReadable()) return; diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMPortForwarder.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMPortForwarder.java new file mode 100644 index 000000000..0504523ff --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMPortForwarder.java @@ -0,0 +1,301 @@ +package cn.classfun.droidvm.daemon.vm; + +import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; +import static cn.classfun.droidvm.lib.utils.ThreadUtils.threadSleep; + +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; + +import cn.classfun.droidvm.daemon.network.NetworkInstance; +import cn.classfun.droidvm.daemon.network.NetworkInstanceStore; +import cn.classfun.droidvm.lib.store.base.DataItem; +import cn.classfun.droidvm.lib.store.vm.VMState; + +/** + * 管理单个 VM 的端口转发(host -> guest)iptables DNAT 规则。 + * VM 进入 RUNNING 后启动:轮询 DHCP 租约/邻居表,按网卡 MAC 解析 guest IP, + * 然后下发规则;VM 退出时撤销全部已下发的规则。 + * + *

规则来源于 VM 配置的 {@code port_forwards} 数组,每项字段: + *

    + *
  • {@code protocol}: tcp | udp(默认 tcp)
  • + *
  • {@code host_port}: 宿主机监听端口(必填)
  • + *
  • {@code guest_port}: guest 目标端口(默认与 host_port 相同)
  • + *
  • {@code host_ip}: 可选,仅转发发往该宿主地址的流量;留空=全部
  • + *
  • {@code network_id}: 可选,多网卡时指定走哪个网卡;留空=首个网卡
  • + *
  • {@code guest_ip}: 可选,手动指定 guest IP,跳过自动发现
  • + *
  • {@code enabled}: 可选,默认 true
  • + *
+ */ +final class VMPortForwarder { + private static final String TAG = "VMPortForwarder"; + private static final int RESOLVE_MAX_ATTEMPTS = 30; + private static final long RESOLVE_INTERVAL_MS = 1000L; + + private final VMInstance vm; + private final NetworkInstanceStore networkStore; + private final List applied = new ArrayList<>(); + private volatile boolean running = false; + private Thread thread; + + VMPortForwarder(@NonNull VMInstance vm, @Nullable NetworkInstanceStore networkStore) { + this.vm = vm; + this.networkStore = networkStore; + } + + boolean hasRules() { + var pf = vm.item.opt("port_forwards", null); + return pf != null && !pf.isEmpty(); + } + + synchronized void start() { + if (running) return; + if (!hasRules()) return; + if (networkStore == null) { + Log.w(TAG, fmt("VM %s has port forwards but no network store", vm.getName())); + return; + } + running = true; + thread = new Thread(this::loop, fmt("PF-%s", vm.getId())); + thread.setDaemon(true); + thread.start(); + } + + synchronized void stop() { + running = false; + var t = thread; + if (t != null) { + t.interrupt(); + thread = null; + } + removeAll(); + } + + @NonNull + JSONArray snapshotApplied() { + var arr = new JSONArray(); + synchronized (applied) { + for (var a : applied) { + try { + var o = new JSONObject(); + o.put("protocol", a.protocol); + o.put("host_ip", a.hostIp == null ? "" : a.hostIp); + o.put("host_port", a.hostPort); + o.put("guest_ip", a.guestIp); + o.put("guest_port", a.guestPort); + arr.put(o); + } catch (JSONException ignored) { + } + } + } + return arr; + } + + private void loop() { + var rules = parseRules(); + if (rules.isEmpty()) return; + int attempts = 0; + while (running && vm.getState() == VMState.RUNNING) { + boolean allDone = true; + for (var rule : rules) { + if (rule.done) continue; + if (!running) return; + var target = resolveTarget(rule); + if (target == null) { + allDone = false; + continue; + } + boolean ok = networkStore.firewall.applyForward( + target.bridge, target.ip, rule.protocol, + rule.hostIp, rule.hostPort, rule.guestPort); + rule.done = true; + if (!ok) { + Log.w(TAG, fmt("VM %s: failed to apply forward %s :%d -> %s:%d", + vm.getName(), rule.protocol, rule.hostPort, target.ip, rule.guestPort)); + continue; + } + // 与 stop()/removeAll() 通过 applied 锁互斥,避免 stop 后仍残留规则 + synchronized (applied) { + if (!running) { + networkStore.firewall.removeForward( + target.bridge, target.ip, rule.protocol, + rule.hostIp, rule.hostPort, rule.guestPort); + } else { + applied.add(new Applied(target.bridge, target.ip, rule.protocol, + rule.hostIp, rule.hostPort, rule.guestPort)); + Log.i(TAG, fmt("VM %s: forward %s :%d -> %s:%d", + vm.getName(), rule.protocol, rule.hostPort, target.ip, rule.guestPort)); + } + } + } + if (allDone) break; + if (++attempts >= RESOLVE_MAX_ATTEMPTS) { + Log.w(TAG, fmt("VM %s: gave up resolving guest IP for some port forwards after %d attempts", + vm.getName(), attempts)); + break; + } + threadSleep(RESOLVE_INTERVAL_MS); + } + } + + private void removeAll() { + synchronized (applied) { + if (networkStore != null) + for (var a : applied) + networkStore.firewall.removeForward( + a.bridge, a.guestIp, a.protocol, a.hostIp, a.hostPort, a.guestPort); + applied.clear(); + } + } + + @NonNull + private List parseRules() { + var list = new ArrayList(); + var pf = vm.item.opt("port_forwards", null); + if (pf == null || !pf.is(DataItem.Type.ARRAY)) return list; + var seen = new HashSet(); + for (var iter : pf) { + var r = iter.getValue(); + if (!r.is(DataItem.Type.OBJECT)) continue; + if (!r.optBoolean("enabled", true)) continue; + var protocol = r.optString("protocol", "tcp"); + if (protocol == null || protocol.isEmpty()) protocol = "tcp"; + protocol = protocol.toLowerCase(Locale.ROOT); + if (!protocol.equals("tcp") && !protocol.equals("udp")) continue; + long hostPort = r.optLong("host_port", 0); + if (hostPort <= 0 || hostPort > 65535) continue; + long guestPort = r.optLong("guest_port", 0); + if (guestPort <= 0 || guestPort > 65535) guestPort = hostPort; + var hostIp = r.optString("host_ip", ""); + if (hostIp == null) hostIp = ""; + // 同一 VM 内按 (protocol, host_ip, host_port) 去重,避免重复 DNAT + var key = fmt("%s|%s|%d", protocol, hostIp, hostPort); + if (!seen.add(key)) { + Log.w(TAG, fmt("VM %s: duplicate port forward %s, skipping", vm.getName(), key)); + continue; + } + var rule = new Rule(); + rule.protocol = protocol; + rule.hostIp = hostIp; + rule.hostPort = (int) hostPort; + rule.guestPort = (int) guestPort; + rule.networkId = r.optString("network_id", ""); + rule.fixedGuestIp = r.optString("guest_ip", ""); + list.add(rule); + } + return list; + } + + @Nullable + private Target resolveTarget(@NonNull Rule rule) { + var net = findNetwork(rule.networkId); + if (net == null) return null; + var netId = net.optString("network_id", ""); + if (netId == null || netId.isEmpty()) return null; + var netInst = networkStore.findById(netId); + if (netInst == null) return null; + var bridge = netInst.item.optString("bridge_name", ""); + if (bridge == null || bridge.isEmpty()) return null; + if (rule.fixedGuestIp != null && !rule.fixedGuestIp.isEmpty()) + return new Target(bridge, rule.fixedGuestIp); + var mac = net.optString("mac_address", ""); + if (mac == null || mac.isEmpty()) return null; + var ip = resolveGuestIpByMac(netInst, bridge, mac); + if (ip == null) return null; + return new Target(bridge, ip); + } + + @Nullable + private String resolveGuestIpByMac( + @NonNull NetworkInstance netInst, @NonNull String bridge, @NonNull String mac) { + var macLower = mac.toLowerCase(Locale.ROOT); + // 1. 优先用 dnsmasq DHCP 租约 + var ip = matchMac(networkStore.backend.listDhcpLeases(bridge), macLower, "ip", "mac"); + if (ip != null) return ip; + // 2. 回退到 ARP 邻居表(适用于静态 IP 且已通信过的 guest) + return matchMac(netInst.listNeighbors(), macLower, "dst", "lladdr"); + } + + @Nullable + private static String matchMac( + @Nullable JSONArray arr, @NonNull String macLower, + @NonNull String ipKey, @NonNull String macKey) { + if (arr == null) return null; + for (int i = 0; i < arr.length(); i++) { + var o = arr.optJSONObject(i); + if (o == null) continue; + var m = o.optString(macKey, ""); + if (!m.isEmpty() && m.toLowerCase(Locale.ROOT).equals(macLower)) { + var ip = o.optString(ipKey, ""); + if (!ip.isEmpty()) return ip; + } + } + return null; + } + + @Nullable + private DataItem findNetwork(@Nullable String networkId) { + var nets = vm.item.opt("networks", null); + if (nets == null || !nets.is(DataItem.Type.ARRAY)) return null; + DataItem first = null; + for (var iter : nets) { + var net = iter.getValue(); + var nid = net.optString("network_id", ""); + if (nid == null || nid.isEmpty()) continue; + if (first == null) first = net; + if (networkId == null || networkId.isEmpty()) return net; + if (networkId.equals(nid)) return net; + } + return first; + } + + private static final class Applied { + final String bridge; + final String guestIp; + final String protocol; + final String hostIp; + final int hostPort; + final int guestPort; + + Applied(String bridge, String guestIp, String protocol, + String hostIp, int hostPort, int guestPort) { + this.bridge = bridge; + this.guestIp = guestIp; + this.protocol = protocol; + this.hostIp = hostIp; + this.hostPort = hostPort; + this.guestPort = guestPort; + } + } + + private static final class Rule { + String protocol; + String hostIp; + int hostPort; + int guestPort; + String networkId; + String fixedGuestIp; + boolean done = false; + } + + private static final class Target { + final String bridge; + final String ip; + + Target(String bridge, String ip) { + this.bridge = bridge; + this.ip = ip; + } + } +} From dd94627058d0b4896265e23f350100361ffa65ee Mon Sep 17 00:00:00 2001 From: lateautumn233 Date: Sun, 31 May 2026 03:47:20 +0800 Subject: [PATCH 002/182] feat(vm): add comprehensive port forwarding UI and management - Add PortForwardSetHandler for IPC communication - Implement VMEditPortForwardTab for editing port forwards - Add PortForwardEditAdapter and ViewHolder for RecyclerView - Enhance VMInfoActivity to display active port forwards - Create PortProtocol enum for TCP/UDP support - Add UI layouts for port forward editing and display - Add localized strings for port forwarding features This commit introduces a complete port forwarding management system with both edit-time configuration and runtime display. --- .../daemon/ipc/vm/PortForwardSetHandler.java | 41 ++++ .../droidvm/daemon/vm/VMInstance.java | 11 + .../droidvm/daemon/vm/VMPortForwarder.java | 84 +++++++ .../droidvm/lib/store/vm/PortProtocol.java | 16 ++ .../droidvm/ui/vm/edit/base/VMEditTab.java | 6 + .../portforward/PortForwardEditAdapter.java | 174 +++++++++++++++ .../PortForwardEditViewHolder.java | 66 ++++++ .../portforward/VMEditPortForwardTab.java | 66 ++++++ .../droidvm/ui/vm/info/VMInfoActivity.java | 134 +++++++++++ app/src/main/res/layout/activity_vm_edit.xml | 7 + app/src/main/res/layout/activity_vm_info.xml | 39 ++++ .../res/layout/dialog_port_forward_edit.xml | 16 ++ .../res/layout/item_port_forward_active.xml | 29 +++ .../res/layout/item_port_forward_edit.xml | 210 ++++++++++++++++++ .../layout/partial_vm_edit_port_forward.xml | 22 ++ app/src/main/res/values-zh-rCN/strings.xml | 20 ++ app/src/main/res/values/strings.xml | 20 ++ 17 files changed, 961 insertions(+) create mode 100644 app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/PortForwardSetHandler.java create mode 100644 app/src/main/java/cn/classfun/droidvm/lib/store/vm/PortProtocol.java create mode 100644 app/src/main/java/cn/classfun/droidvm/ui/vm/edit/portforward/PortForwardEditAdapter.java create mode 100644 app/src/main/java/cn/classfun/droidvm/ui/vm/edit/portforward/PortForwardEditViewHolder.java create mode 100644 app/src/main/java/cn/classfun/droidvm/ui/vm/edit/portforward/VMEditPortForwardTab.java create mode 100644 app/src/main/res/layout/dialog_port_forward_edit.xml create mode 100644 app/src/main/res/layout/item_port_forward_active.xml create mode 100644 app/src/main/res/layout/item_port_forward_edit.xml create mode 100644 app/src/main/res/layout/partial_vm_edit_port_forward.xml diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/PortForwardSetHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/PortForwardSetHandler.java new file mode 100644 index 000000000..1a57f2119 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/PortForwardSetHandler.java @@ -0,0 +1,41 @@ +package cn.classfun.droidvm.daemon.ipc.vm; + +import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; + +import androidx.annotation.NonNull; + +import com.google.auto.service.AutoService; + +import org.json.JSONArray; +import org.json.JSONObject; + +import cn.classfun.droidvm.daemon.server.ClientRequest; +import cn.classfun.droidvm.daemon.server.RequestException; +import cn.classfun.droidvm.daemon.server.RequestHandler; + +/** IPC: hot-applies {@code rules} to a running VM ({@code vm_id}); the frontend owns persistence. */ +@AutoService(RequestHandler.class) +public final class PortForwardSetHandler extends RequestHandler { + @NonNull + @Override + public String getName() { + return "vm_port_forward_set"; + } + + @Override + public void handle(@NonNull ClientRequest request) throws Exception { + var params = request.getParams(); + var id = params.optString("vm_id", ""); + if (id.isEmpty()) + throw new RequestException("missing vm_id"); + var vm = request.getContext().getVMs().findById(id); + if (vm == null) + throw new RequestException(fmt("VM %s not found", id)); + var rules = params.optJSONArray("rules"); + if (rules == null) rules = new JSONArray(); + vm.applyPortForwards(rules); + var data = new JSONObject(); + data.put("active", vm.getActivePortForwards()); + request.res().put("data", data); + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMInstance.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMInstance.java index c5e62e0b5..374a86cab 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMInstance.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMInstance.java @@ -404,6 +404,17 @@ public JSONArray getActivePortForwards() { return pf != null ? pf.snapshotApplied() : new JSONArray(); } + /** + * 运行时热更新端口转发规则:更新内存配置后即时同步到 iptables(增删 DNAT 规则)。 + * 仅作用于 daemon 运行态;配置的持久化由前端负责(写入共享的 vms.json), + * 下次启动时前端会通过 vm_modify 重新下发完整配置。 + */ + public void applyPortForwards(@NonNull JSONArray rules) { + item.set("port_forwards", rules); + var pf = portForwarder; + if (pf != null) pf.sync(); + } + private void startReaderThread(@NonNull String vmId, @NonNull ConsoleStream stream) { var streamName = stream.getName(); if (!stream.isReadable()) return; diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMPortForwarder.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMPortForwarder.java index 0504523ff..8fcace647 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMPortForwarder.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMPortForwarder.java @@ -102,6 +102,90 @@ JSONArray snapshotApplied() { return arr; } + /** + * 运行时热同步当前配置: + *
    + *
  • 已在转发(VM 启动时已有规则)→ {@link #reapply()} 做增量 diff;
  • + *
  • 此前因无规则未启动 → {@link #start()} 按新配置启动轮询应用。
  • + *
+ * 供 {@link VMInstance#applyPortForwards} 在运行时改规则后调用。 + */ + synchronized void sync() { + if (running) reapply(); + else start(); + } + + /** + * 运行时热更新:基于 VM 当前 {@code port_forwards} 配置对已应用规则做增量 diff—— + * 移除不再需要的、应用新增的。仅在 VM 运行(已 {@link #start()})时有效。 + * 新增规则要求 guest IP 当前可解析(运行中 VM 一般已联网,故可解析)。 + */ + private synchronized void reapply() { + if (!running || networkStore == null) return; + var rules = parseRules(); + // 锁外解析目标,避免持 applied 锁期间做 DHCP/邻居表查询 + var desired = new ArrayList(); + for (var rule : rules) { + var target = resolveTarget(rule); + if (target == null) { + Log.w(TAG, fmt("VM %s: reapply skipped unresolved %s :%d (guest IP not ready)", + vm.getName(), rule.protocol, rule.hostPort)); + continue; + } + desired.add(new Applied(target.bridge, target.ip, rule.protocol, + rule.hostIp, rule.hostPort, rule.guestPort)); + } + synchronized (applied) { + applied.removeIf(a -> { + boolean keep = false; + for (var d : desired) + if (sameForward(a, d)) { + keep = true; + break; + } + if (!keep) { + networkStore.firewall.removeForward( + a.bridge, a.guestIp, a.protocol, a.hostIp, a.hostPort, a.guestPort); + Log.i(TAG, fmt("VM %s: reapply removed %s :%d -> %s:%d", + vm.getName(), a.protocol, a.hostPort, a.guestIp, a.guestPort)); + } + return !keep; + }); + for (var d : desired) { + boolean exists = false; + for (var a : applied) + if (sameForward(a, d)) { + exists = true; + break; + } + if (exists) continue; + boolean ok = networkStore.firewall.applyForward( + d.bridge, d.guestIp, d.protocol, d.hostIp, d.hostPort, d.guestPort); + if (ok) { + applied.add(d); + Log.i(TAG, fmt("VM %s: reapply added %s :%d -> %s:%d", + vm.getName(), d.protocol, d.hostPort, d.guestIp, d.guestPort)); + } else { + Log.w(TAG, fmt("VM %s: reapply failed to apply %s :%d", + vm.getName(), d.protocol, d.hostPort)); + } + } + } + } + + private static boolean sameForward(@NonNull Applied a, @NonNull Applied b) { + return a.protocol.equals(b.protocol) + && a.hostPort == b.hostPort + && a.guestPort == b.guestPort + && eq(a.hostIp, b.hostIp) + && eq(a.guestIp, b.guestIp) + && eq(a.bridge, b.bridge); + } + + private static boolean eq(@Nullable String a, @Nullable String b) { + return a == null ? b == null : a.equals(b); + } + private void loop() { var rules = parseRules(); if (rules.isEmpty()) return; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/PortProtocol.java b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/PortProtocol.java new file mode 100644 index 000000000..edca416f7 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/PortProtocol.java @@ -0,0 +1,16 @@ +package cn.classfun.droidvm.lib.store.vm; + +import androidx.annotation.NonNull; + +import cn.classfun.droidvm.lib.store.enums.StringEnum; + +public enum PortProtocol implements StringEnum { + TCP, + UDP; + + @NonNull + @Override + public String getString() { + return name(); + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/base/VMEditTab.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/base/VMEditTab.java index 93a936821..ffe193a83 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/base/VMEditTab.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/base/VMEditTab.java @@ -17,6 +17,7 @@ import cn.classfun.droidvm.ui.vm.edit.boot.VMEditBootTab; import cn.classfun.droidvm.ui.vm.edit.graphics.VMEditGraphicsTab; import cn.classfun.droidvm.ui.vm.edit.network.VMEditNetworkTab; +import cn.classfun.droidvm.ui.vm.edit.portforward.VMEditPortForwardTab; import cn.classfun.droidvm.ui.vm.edit.storage.VMEditStorageTab; public enum VMEditTab implements StringEnum { @@ -40,6 +41,11 @@ public enum VMEditTab implements StringEnum { R.id.tab_content_network, VMEditNetworkTab.class ), + TAB_PORT_FORWARD( + R.string.create_vm_tab_port_forward, + R.id.tab_content_port_forward, + VMEditPortForwardTab.class + ), TAB_GRAPHICS( R.string.create_vm_tab_graphics, R.id.tab_content_graphics, diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/portforward/PortForwardEditAdapter.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/portforward/PortForwardEditAdapter.java new file mode 100644 index 000000000..beae8a645 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/portforward/PortForwardEditAdapter.java @@ -0,0 +1,174 @@ +package cn.classfun.droidvm.ui.vm.edit.portforward; + +import android.content.Context; +import android.content.DialogInterface; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.button.MaterialButton; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +import java.util.ArrayList; + +import cn.classfun.droidvm.R; +import cn.classfun.droidvm.lib.store.base.DataItem; +import cn.classfun.droidvm.lib.store.enums.Enums; +import cn.classfun.droidvm.lib.store.network.NetworkStore; +import cn.classfun.droidvm.lib.store.vm.PortProtocol; +import cn.classfun.droidvm.lib.ui.SimpleTextWatcher; +import cn.classfun.droidvm.ui.widgets.container.CardItemAdapter; + +public final class PortForwardEditAdapter extends CardItemAdapter { + public PortForwardEditAdapter(@NonNull Context context) { + super(context); + } + + @NonNull + @Override + protected PortForwardEditViewHolder createViewHolderInstance(@NonNull View view) { + return new PortForwardEditViewHolder(view); + } + + @Override + protected int getLayoutRes() { + return R.layout.item_port_forward_edit; + } + + @Override + public void onBindViewHolder(@NonNull PortForwardEditViewHolder holder, int position) { + var item = items.get(position); + holder.unbindWatchers(); + + holder.btnProtocol.configure( + PortProtocol.class, Enums.optEnum(item, "protocol", PortProtocol.TCP)); + holder.btnProtocol.setOnValueChangedListener((oldVal, newVal) -> { + int pos = holder.getBindingAdapterPosition(); + if (pos != RecyclerView.NO_POSITION) + items.get(pos).set("protocol", newVal); + }); + + long hostPort = item.optLong("host_port", 0); + holder.etHostPort.setText(hostPort > 0 ? String.valueOf(hostPort) : ""); + holder.hostPortWatcher = SimpleTextWatcher.simpleAfterTextWatcher(s -> { + int pos = holder.getBindingAdapterPosition(); + if (pos != RecyclerView.NO_POSITION) + setPort(items.get(pos), "host_port", s.toString()); + }); + holder.etHostPort.addTextChangedListener(holder.hostPortWatcher); + + // Guest port (empty = same as host port, backend falls back) + long guestPort = item.optLong("guest_port", 0); + holder.etGuestPort.setText(guestPort > 0 ? String.valueOf(guestPort) : ""); + holder.guestPortWatcher = SimpleTextWatcher.simpleAfterTextWatcher(s -> { + int pos = holder.getBindingAdapterPosition(); + if (pos != RecyclerView.NO_POSITION) + setPort(items.get(pos), "guest_port", s.toString()); + }); + holder.etGuestPort.addTextChangedListener(holder.guestPortWatcher); + + holder.switchEnabled.setOnCheckedChangeListener(null); + holder.switchEnabled.setChecked(item.optBoolean("enabled", true)); + holder.switchEnabled.setOnCheckedChangeListener((btn, checked) -> { + int pos = holder.getBindingAdapterPosition(); + if (pos != RecyclerView.NO_POSITION) + items.get(pos).set("enabled", checked); + }); + + holder.etHostIp.setText(item.optString("host_ip", "")); + holder.hostIpWatcher = SimpleTextWatcher.simpleAfterTextWatcher(s -> { + int pos = holder.getBindingAdapterPosition(); + if (pos != RecyclerView.NO_POSITION) + items.get(pos).set("host_ip", s.toString().trim()); + }); + holder.etHostIp.addTextChangedListener(holder.hostIpWatcher); + + holder.etGuestIp.setText(item.optString("guest_ip", "")); + holder.guestIpWatcher = SimpleTextWatcher.simpleAfterTextWatcher(s -> { + int pos = holder.getBindingAdapterPosition(); + if (pos != RecyclerView.NO_POSITION) + items.get(pos).set("guest_ip", s.toString().trim()); + }); + holder.etGuestIp.addTextChangedListener(holder.guestIpWatcher); + + updateNetworkButton(holder.btnNetwork, item.optString("network_id", "")); + holder.btnNetwork.setOnClickListener(v -> { + int pos = holder.getBindingAdapterPosition(); + if (pos != RecyclerView.NO_POSITION) + showNetworkPicker(pos, holder.btnNetwork); + }); + + // Advanced section expand state: expanded by default when advanced values exist (data-driven, safe to reuse) + boolean hasAdvanced = !item.optString("host_ip", "").isEmpty() + || !item.optString("guest_ip", "").isEmpty() + || !item.optString("network_id", "").isEmpty(); + applyAdvancedState(holder, hasAdvanced); + holder.btnAdvanced.setOnClickListener(v -> { + boolean show = holder.layoutAdvanced.getVisibility() != View.VISIBLE; + applyAdvancedState(holder, show); + }); + + holder.btnDelete.setOnClickListener(v -> { + int pos = holder.getBindingAdapterPosition(); + if (pos != RecyclerView.NO_POSITION) + removeItem(pos); + }); + } + + private void applyAdvancedState(@NonNull PortForwardEditViewHolder holder, boolean expanded) { + holder.layoutAdvanced.setVisibility(expanded ? View.VISIBLE : View.GONE); + holder.btnAdvanced.setIconResource( + expanded ? R.drawable.ic_expand_less : R.drawable.ic_expand_more); + } + + /** Writes the port as an integer; empty/invalid becomes 0 (backend: host_port=0 is skipped, guest_port=0 falls back to host_port). */ + private void setPort(@NonNull DataItem item, @NonNull String key, @NonNull String text) { + text = text.trim(); + if (text.isEmpty()) { + item.set(key, 0L); + return; + } + try { + item.set(key, (long) Integer.parseInt(text)); + } catch (NumberFormatException e) { + item.set(key, 0L); + } + } + + private void showNetworkPicker(int position, @NonNull MaterialButton btn) { + var netStore = new NetworkStore(); + netStore.load(context); + var ids = new ArrayList(); + var names = new ArrayList(); + ids.add(""); + names.add(context.getString(R.string.create_vm_network_none)); + netStore.forEach((id, config) -> { + ids.add(id.toString()); + names.add(config.getName()); + }); + var currentId = items.get(position).optString("network_id", ""); + int checked = Math.max(0, ids.indexOf(currentId)); + DialogInterface.OnClickListener onclick = (dialog, which) -> { + items.get(position).set("network_id", ids.get(which)); + updateNetworkButton(btn, ids.get(which)); + dialog.dismiss(); + }; + new MaterialAlertDialogBuilder(context) + .setTitle(R.string.edit_vm_pf_network) + .setSingleChoiceItems(names.toArray(new String[0]), checked, onclick) + .show(); + } + + private void updateNetworkButton(@NonNull MaterialButton btn, String networkId) { + if (networkId == null || networkId.isEmpty()) { + btn.setText(R.string.create_vm_network_none); + return; + } + var netStore = new NetworkStore(); + netStore.load(context); + var net = netStore.findById(networkId); + btn.setText(net != null ? net.getName() : + context.getString(R.string.create_vm_network_none)); + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/portforward/PortForwardEditViewHolder.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/portforward/PortForwardEditViewHolder.java new file mode 100644 index 000000000..31e37abab --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/portforward/PortForwardEditViewHolder.java @@ -0,0 +1,66 @@ +package cn.classfun.droidvm.ui.vm.edit.portforward; + +import android.text.TextWatcher; +import android.view.View; +import android.widget.ImageButton; +import android.widget.LinearLayout; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.button.MaterialButton; +import com.google.android.material.materialswitch.MaterialSwitch; +import com.google.android.material.textfield.TextInputEditText; + +import cn.classfun.droidvm.R; +import cn.classfun.droidvm.ui.widgets.tools.PickerButtonWidget; + +public final class PortForwardEditViewHolder extends RecyclerView.ViewHolder { + final TextInputEditText etHostPort; + final TextInputEditText etGuestPort; + final TextInputEditText etHostIp; + final TextInputEditText etGuestIp; + final PickerButtonWidget btnProtocol; + final MaterialSwitch switchEnabled; + final MaterialButton btnAdvanced; + final MaterialButton btnNetwork; + final ImageButton btnDelete; + final LinearLayout layoutAdvanced; + TextWatcher hostPortWatcher; + TextWatcher guestPortWatcher; + TextWatcher hostIpWatcher; + TextWatcher guestIpWatcher; + + PortForwardEditViewHolder(@NonNull View itemView) { + super(itemView); + etHostPort = itemView.findViewById(R.id.et_pf_host_port); + etGuestPort = itemView.findViewById(R.id.et_pf_guest_port); + etHostIp = itemView.findViewById(R.id.et_pf_host_ip); + etGuestIp = itemView.findViewById(R.id.et_pf_guest_ip); + btnProtocol = itemView.findViewById(R.id.btn_pf_protocol); + switchEnabled = itemView.findViewById(R.id.switch_pf_enabled); + btnAdvanced = itemView.findViewById(R.id.btn_pf_advanced); + btnNetwork = itemView.findViewById(R.id.btn_pf_network); + btnDelete = itemView.findViewById(R.id.btn_pf_delete); + layoutAdvanced = itemView.findViewById(R.id.layout_pf_advanced); + } + + void unbindWatchers() { + if (hostPortWatcher != null) { + etHostPort.removeTextChangedListener(hostPortWatcher); + hostPortWatcher = null; + } + if (guestPortWatcher != null) { + etGuestPort.removeTextChangedListener(guestPortWatcher); + guestPortWatcher = null; + } + if (hostIpWatcher != null) { + etHostIp.removeTextChangedListener(hostIpWatcher); + hostIpWatcher = null; + } + if (guestIpWatcher != null) { + etGuestIp.removeTextChangedListener(guestIpWatcher); + guestIpWatcher = null; + } + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/portforward/VMEditPortForwardTab.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/portforward/VMEditPortForwardTab.java new file mode 100644 index 000000000..d0256a9fb --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/portforward/VMEditPortForwardTab.java @@ -0,0 +1,66 @@ +package cn.classfun.droidvm.ui.vm.edit.portforward; + +import static java.util.Objects.requireNonNull; + +import android.view.View; +import android.widget.Toast; + +import androidx.annotation.NonNull; + +import cn.classfun.droidvm.R; +import cn.classfun.droidvm.lib.store.base.DataItem; +import cn.classfun.droidvm.lib.store.vm.VMConfig; +import cn.classfun.droidvm.lib.store.vm.VMStore; +import cn.classfun.droidvm.ui.vm.edit.VMEditActivity; +import cn.classfun.droidvm.ui.vm.edit.base.VMEditBaseTab; +import cn.classfun.droidvm.ui.widgets.container.CardItemListView; + +public final class VMEditPortForwardTab extends VMEditBaseTab { + private CardItemListView listPf; + + public VMEditPortForwardTab(VMEditActivity parent, View view) { + super(parent, view); + } + + @Override + public void initView() { + listPf = view.findViewById(R.id.list_port_forwards); + } + + @Override + public void initValue() { + listPf.setAdapter(PortForwardEditAdapter.class); + } + + @Override + public void loadConfig(@NonNull VMConfig config) { + listPf.setItems(config.item.opt("port_forwards", DataItem.newArray())); + } + + @Override + public boolean validateInput(@NonNull VMStore store) { + if (!rulesValid(requireNonNull(listPf.getItems()))) { + Toast.makeText(parent, R.string.edit_vm_pf_invalid_port, Toast.LENGTH_SHORT).show(); + return false; + } + return true; + } + + /** Mirrors the backend VMPortForwarder.parseRules checks (enabled rules only). */ + public static boolean rulesValid(@NonNull DataItem rules) { + for (var iter : rules) { + var r = iter.getValue(); + if (!r.optBoolean("enabled", true)) continue; + long hostPort = r.optLong("host_port", 0); + if (hostPort <= 0 || hostPort > 65535) return false; + long guestPort = r.optLong("guest_port", 0); + if (guestPort > 65535) return false; // 0 is valid: backend falls back to host_port + } + return true; + } + + @Override + public void saveConfig(@NonNull VMConfig config) { + config.item.set("port_forwards", listPf.getItems()); + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/info/VMInfoActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/info/VMInfoActivity.java index bbfbf2be9..57619d103 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/info/VMInfoActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/info/VMInfoActivity.java @@ -15,10 +15,14 @@ import android.os.Handler; import android.os.Looper; import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import com.google.android.material.appbar.CollapsingToolbarLayout; @@ -26,6 +30,8 @@ import com.google.android.material.button.MaterialButton; import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import org.json.JSONArray; +import org.json.JSONException; import org.json.JSONObject; import java.text.DateFormat; @@ -46,6 +52,10 @@ import cn.classfun.droidvm.lib.ui.UIContext; import cn.classfun.droidvm.ui.vm.VMActions; import cn.classfun.droidvm.ui.vm.edit.VMEditActivity; +import cn.classfun.droidvm.ui.vm.edit.portforward.PortForwardEditAdapter; +import cn.classfun.droidvm.ui.vm.edit.portforward.VMEditPortForwardTab; +import cn.classfun.droidvm.ui.widgets.container.CardItemListView; +import cn.classfun.droidvm.ui.widgets.container.CollapsibleContainer; import cn.classfun.droidvm.ui.widgets.row.TextRowWidget; public final class VMInfoActivity extends AppCompatActivity implements ForegroundCallback { @@ -74,6 +84,10 @@ public final class VMInfoActivity extends AppCompatActivity implements Foregroun private TextRowWidget rowDisks; private TextRowWidget rowOptions; private TextRowWidget rowCreated; + private CollapsibleContainer ccPortForwards; + private LinearLayout containerPortForwards; + private TextView tvPfEmpty; + private MaterialButton btnPfEdit; public VMState currentState = VMState.STOPPED; public UUID vmId; public VMConfig config; @@ -101,6 +115,10 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { rowDisks = findViewById(R.id.row_disks); rowOptions = findViewById(R.id.row_options); rowCreated = findViewById(R.id.row_created); + ccPortForwards = findViewById(R.id.cc_port_forwards); + containerPortForwards = findViewById(R.id.container_port_forwards); + tvPfEmpty = findViewById(R.id.tv_pf_empty); + btnPfEdit = findViewById(R.id.btn_pf_edit); toolbar = findViewById(R.id.toolbar); initialize(); } @@ -125,6 +143,7 @@ private void initialize() { sendControlCommand("powerbtn", vmId, mainHandler, ui)); btnSleepBtn.setOnClickListener(v -> sendControlCommand("sleepbtn", vmId, mainHandler, ui)); + btnPfEdit.setOnClickListener(v -> showPortForwardEditor()); } @Override @@ -293,6 +312,121 @@ private void updateStateUI() { } else { tvStateDetail.setText(""); } + refreshPortForwards(); + if (isRunning) + mainHandler.postDelayed(this::refreshPortForwards, 2000); + } + + private void refreshPortForwards() { + if (currentState != VMState.RUNNING) { + ccPortForwards.setVisibility(View.GONE); + return; + } + DaemonConnection.OnResponse res = resp -> { + var data = resp.optJSONObject("data"); + var active = data != null ? data.optJSONArray("active") : null; + mainHandler.post(() -> renderPortForwards(active)); + }; + DaemonConnection.getInstance().buildRequest("vm_port_forward_list") + .put("vm_id", vmId.toString()) + .onResponse(res) + .onUnsuccessful(r -> { + }) + .onError(e -> Log.w(TAG, "Failed to query port forwards", e)) + .invoke(); + } + + private void renderPortForwards(@Nullable JSONArray active) { + if (isFinishing()) return; + if (currentState != VMState.RUNNING) { + ccPortForwards.setVisibility(View.GONE); + return; + } + ccPortForwards.setVisibility(View.VISIBLE); + containerPortForwards.removeAllViews(); + int count = active != null ? active.length() : 0; + if (count == 0) { + tvPfEmpty.setVisibility(View.VISIBLE); + return; + } + tvPfEmpty.setVisibility(View.GONE); + var inflater = LayoutInflater.from(this); + for (int i = 0; i < count; i++) { + var o = active.optJSONObject(i); + if (o == null) continue; + var protocol = o.optString("protocol", "tcp"); + var proto = "udp".equals(protocol) ? "UDP" : "TCP"; + var hostIp = o.optString("host_ip", ""); + var hostPort = o.optInt("host_port", 0); + var guestIp = o.optString("guest_ip", ""); + var guestPort = o.optInt("guest_port", 0); + var hostDisplay = (hostIp.isEmpty() ? "*" : hostIp) + ":" + hostPort; + var line = proto + " " + hostDisplay + " -> " + guestIp + ":" + guestPort; + var row = inflater.inflate( + R.layout.item_port_forward_active, containerPortForwards, false); + ((TextView) row.findViewById(R.id.tv_pf_line)).setText(line); + containerPortForwards.addView(row); + } + } + + private void showPortForwardEditor() { + if (config == null) return; + var view = LayoutInflater.from(this).inflate(R.layout.dialog_port_forward_edit, null); + CardItemListView list = view.findViewById(R.id.list_port_forwards); + list.setAdapter(PortForwardEditAdapter.class); + list.setItems(copyArray(config.item.opt("port_forwards", DataItem.newArray()))); + var dialog = new MaterialAlertDialogBuilder(this) + .setTitle(R.string.vm_info_port_forwards) + .setView(view) + .setPositiveButton(R.string.edit_vm_pf_apply, null) + .setNegativeButton(android.R.string.cancel, null) + .create(); + // Override the button so failed validation keeps the dialog open. + dialog.setOnShowListener(d -> dialog.getButton(AlertDialog.BUTTON_POSITIVE) + .setOnClickListener(v -> { + if (applyPortForwardEdits(list)) dialog.dismiss(); + })); + dialog.show(); + } + + private boolean applyPortForwardEdits(CardItemListView list) { + var items = list.getItems(); + if (items == null) items = DataItem.newArray(); + if (!VMEditPortForwardTab.rulesValid(items)) { + Toast.makeText(this, R.string.edit_vm_pf_invalid_port, Toast.LENGTH_SHORT).show(); + return false; + } + config.item.set("port_forwards", items); + runOnPool(() -> store.save(this)); + JSONArray rules; + try { + rules = items.toJsonArray(); + } catch (JSONException e) { + rules = new JSONArray(); + } + DaemonConnection.getInstance().buildRequest("vm_port_forward_set") + .put("vm_id", vmId.toString()) + .put("rules", rules) + .onResponse(r -> { + // Apply may take effect sync (reconcile) or async (loop start); refresh now and after a delay. + mainHandler.post(this::refreshPortForwards); + mainHandler.postDelayed(this::refreshPortForwards, 1500); + }) + .onUnsuccessful(r -> { + }) + .onError(e -> Log.w(TAG, "Failed to set port forwards", e)) + .invoke(); + return true; + } + + /** Deep copy so a cancelled dialog doesn't mutate config. */ + private static DataItem copyArray(DataItem src) { + if (!src.is(DataItem.Type.ARRAY)) return DataItem.newArray(); + try { + return DataItem.fromJson(src.toJsonArray()); + } catch (JSONException e) { + return DataItem.newArray(); + } } private void onStartStopClicked() { diff --git a/app/src/main/res/layout/activity_vm_edit.xml b/app/src/main/res/layout/activity_vm_edit.xml index b51364da8..0f503cf47 100644 --- a/app/src/main/res/layout/activity_vm_edit.xml +++ b/app/src/main/res/layout/activity_vm_edit.xml @@ -81,6 +81,13 @@ android:layout_height="wrap_content" android:visibility="gone" /> + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_port_forward_edit.xml b/app/src/main/res/layout/dialog_port_forward_edit.xml new file mode 100644 index 000000000..cdaa096d6 --- /dev/null +++ b/app/src/main/res/layout/dialog_port_forward_edit.xml @@ -0,0 +1,16 @@ + + + + + + diff --git a/app/src/main/res/layout/item_port_forward_active.xml b/app/src/main/res/layout/item_port_forward_active.xml new file mode 100644 index 000000000..28dac99ad --- /dev/null +++ b/app/src/main/res/layout/item_port_forward_active.xml @@ -0,0 +1,29 @@ + + + + + + + + diff --git a/app/src/main/res/layout/item_port_forward_edit.xml b/app/src/main/res/layout/item_port_forward_edit.xml new file mode 100644 index 000000000..f5badd167 --- /dev/null +++ b/app/src/main/res/layout/item_port_forward_edit.xml @@ -0,0 +1,210 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/partial_vm_edit_port_forward.xml b/app/src/main/res/layout/partial_vm_edit_port_forward.xml new file mode 100644 index 000000000..dd5f659db --- /dev/null +++ b/app/src/main/res/layout/partial_vm_edit_port_forward.xml @@ -0,0 +1,22 @@ + + + + + + + + + + diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 68047ef64..c44049740 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -224,6 +224,26 @@ 移除网络 MAC 地址(可选) 随机 + + 端口 + 端口转发 + 添加规则 + 暂无端口转发规则。 + 删除规则 + 协议 + 主机端口 + Guest 端口 + → + 启用 + 高级 + 主机 IP(可选) + Guest IP(留空自动发现) + 网卡 + 主机端口须为 1-65535 + 端口转发 + 暂无生效转发 + 编辑转发 + 应用 路径 只读 总线 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7f83b1c36..36cb6cdfc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -226,6 +226,26 @@ Remove network MAC address (optional) Random + + Ports + Port Forwarding + Add Rule + No port forwarding rules. + Remove rule + Protocol + Host Port + Guest Port + → + Enabled + Advanced + Host IP (optional) + Guest IP (auto if empty) + Network + Host port must be 1-65535 + Port Forwarding + No active forwards + Edit Forwards + Apply Path Read-only Bus From bea0c6f4790844d030eb0c0d411ac3e51d477119 Mon Sep 17 00:00:00 2001 From: lateautumn233 Date: Mon, 1 Jun 2026 01:40:46 +0800 Subject: [PATCH 003/182] fix(vm): refactor port forwarder for event-driven config sync - Add volatile modifier to ensure thread visibility - Replace polling-based loop with wait/notify event model - Refactor reconcile logic with atomic rule switching and better error handling - Improve handling of unresolved guest IPs across retry attempts --- .../droidvm/daemon/vm/VMInstance.java | 8 +- .../droidvm/daemon/vm/VMPortForwarder.java | 152 ++++++++---------- 2 files changed, 65 insertions(+), 95 deletions(-) diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMInstance.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMInstance.java index 374a86cab..114250ea3 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMInstance.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMInstance.java @@ -42,7 +42,7 @@ public final class VMInstance extends VMConfig { private BackendBase backend; private VMBackendInstance backendInstance; private Thread workerThread; - private VMPortForwarder portForwarder; + private volatile VMPortForwarder portForwarder; private final VMInstanceStore store; public interface VMEventCallback { @@ -404,11 +404,7 @@ public JSONArray getActivePortForwards() { return pf != null ? pf.snapshotApplied() : new JSONArray(); } - /** - * 运行时热更新端口转发规则:更新内存配置后即时同步到 iptables(增删 DNAT 规则)。 - * 仅作用于 daemon 运行态;配置的持久化由前端负责(写入共享的 vms.json), - * 下次启动时前端会通过 vm_modify 重新下发完整配置。 - */ + /** Runtime-only hot-update of DNAT rules; the frontend owns persistence (re-pushed via vm_modify on next start). */ public void applyPortForwards(@NonNull JSONArray rules) { item.set("port_forwards", rules); var pf = portForwarder; diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMPortForwarder.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMPortForwarder.java index 8fcace647..1f675ec4f 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMPortForwarder.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMPortForwarder.java @@ -1,7 +1,6 @@ package cn.classfun.droidvm.daemon.vm; import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; -import static cn.classfun.droidvm.lib.utils.ThreadUtils.threadSleep; import android.util.Log; @@ -23,20 +22,8 @@ import cn.classfun.droidvm.lib.store.vm.VMState; /** - * 管理单个 VM 的端口转发(host -> guest)iptables DNAT 规则。 - * VM 进入 RUNNING 后启动:轮询 DHCP 租约/邻居表,按网卡 MAC 解析 guest IP, - * 然后下发规则;VM 退出时撤销全部已下发的规则。 - * - *

规则来源于 VM 配置的 {@code port_forwards} 数组,每项字段: - *

    - *
  • {@code protocol}: tcp | udp(默认 tcp)
  • - *
  • {@code host_port}: 宿主机监听端口(必填)
  • - *
  • {@code guest_port}: guest 目标端口(默认与 host_port 相同)
  • - *
  • {@code host_ip}: 可选,仅转发发往该宿主地址的流量;留空=全部
  • - *
  • {@code network_id}: 可选,多网卡时指定走哪个网卡;留空=首个网卡
  • - *
  • {@code guest_ip}: 可选,手动指定 guest IP,跳过自动发现
  • - *
  • {@code enabled}: 可选,默认 true
  • - *
+ * Installs host -> guest iptables DNAT rules for one VM's {@code port_forwards}, resolving the + * guest IP from DHCP leases / the neighbor table while the VM is RUNNING. */ final class VMPortForwarder { private static final String TAG = "VMPortForwarder"; @@ -74,6 +61,7 @@ synchronized void start() { synchronized void stop() { running = false; + notifyAll(); var t = thread; if (t != null) { t.interrupt(); @@ -102,34 +90,30 @@ JSONArray snapshotApplied() { return arr; } - /** - * 运行时热同步当前配置: - *
    - *
  • 已在转发(VM 启动时已有规则)→ {@link #reapply()} 做增量 diff;
  • - *
  • 此前因无规则未启动 → {@link #start()} 按新配置启动轮询应用。
  • - *
- * 供 {@link VMInstance#applyPortForwards} 在运行时改规则后调用。 - */ + /** Wakes the poll thread (or starts it) so reconcile always runs on that single thread. */ synchronized void sync() { - if (running) reapply(); + if (running) notifyAll(); else start(); } /** - * 运行时热更新:基于 VM 当前 {@code port_forwards} 配置对已应用规则做增量 diff—— - * 移除不再需要的、应用新增的。仅在 VM 运行(已 {@link #start()})时有效。 - * 新增规则要求 guest IP 当前可解析(运行中 VM 一般已联网,故可解析)。 + * Incrementally reconciles installed rules to the current config. Poll-thread only and the + * sole writer of {@link #applied} (besides {@link #stop()}). A still-configured binding whose + * guest IP is momentarily unresolvable keeps its old entry and is switched atomically once + * resolvable, so hot-editing guest_ip/NIC never drops a working forward. + * + * @return whether any rule is still configured-but-unresolvable (keep polling). */ - private synchronized void reapply() { - if (!running || networkStore == null) return; + private boolean reconcile() { + if (!running || networkStore == null) return false; var rules = parseRules(); - // 锁外解析目标,避免持 applied 锁期间做 DHCP/邻居表查询 + // Resolve outside the applied lock; DHCP / neighbor lookups are slow. var desired = new ArrayList(); + boolean hasUnresolved = false; for (var rule : rules) { var target = resolveTarget(rule); if (target == null) { - Log.w(TAG, fmt("VM %s: reapply skipped unresolved %s :%d (guest IP not ready)", - vm.getName(), rule.protocol, rule.hostPort)); + hasUnresolved = true; continue; } desired.add(new Applied(target.bridge, target.ip, rule.protocol, @@ -137,19 +121,14 @@ private synchronized void reapply() { } synchronized (applied) { applied.removeIf(a -> { - boolean keep = false; for (var d : desired) - if (sameForward(a, d)) { - keep = true; - break; - } - if (!keep) { - networkStore.firewall.removeForward( - a.bridge, a.guestIp, a.protocol, a.hostIp, a.hostPort, a.guestPort); - Log.i(TAG, fmt("VM %s: reapply removed %s :%d -> %s:%d", - vm.getName(), a.protocol, a.hostPort, a.guestIp, a.guestPort)); - } - return !keep; + if (sameForward(a, d)) return false; + if (hostBindingConfigured(a, rules)) return false; + networkStore.firewall.removeForward( + a.bridge, a.guestIp, a.protocol, a.hostIp, a.hostPort, a.guestPort); + Log.i(TAG, fmt("VM %s: removed forward %s :%d -> %s:%d", + vm.getName(), a.protocol, a.hostPort, a.guestIp, a.guestPort)); + return true; }); for (var d : desired) { boolean exists = false; @@ -159,18 +138,27 @@ private synchronized void reapply() { break; } if (exists) continue; + applied.removeIf(a -> { + if (!sameHostBinding(a, d)) return false; + networkStore.firewall.removeForward( + a.bridge, a.guestIp, a.protocol, a.hostIp, a.hostPort, a.guestPort); + Log.i(TAG, fmt("VM %s: switched forward %s :%d off %s:%d", + vm.getName(), a.protocol, a.hostPort, a.guestIp, a.guestPort)); + return true; + }); boolean ok = networkStore.firewall.applyForward( d.bridge, d.guestIp, d.protocol, d.hostIp, d.hostPort, d.guestPort); if (ok) { applied.add(d); - Log.i(TAG, fmt("VM %s: reapply added %s :%d -> %s:%d", + Log.i(TAG, fmt("VM %s: forward %s :%d -> %s:%d", vm.getName(), d.protocol, d.hostPort, d.guestIp, d.guestPort)); } else { - Log.w(TAG, fmt("VM %s: reapply failed to apply %s :%d", + Log.w(TAG, fmt("VM %s: failed to apply forward %s :%d", vm.getName(), d.protocol, d.hostPort)); } } } + return hasUnresolved; } private static boolean sameForward(@NonNull Applied a, @NonNull Applied b) { @@ -182,54 +170,44 @@ && eq(a.guestIp, b.guestIp) && eq(a.bridge, b.bridge); } + private static boolean sameHostBinding(@NonNull Applied a, @NonNull Applied b) { + return a.protocol.equals(b.protocol) && a.hostPort == b.hostPort && eq(a.hostIp, b.hostIp); + } + + private static boolean hostBindingConfigured(@NonNull Applied a, @NonNull List rules) { + for (var r : rules) + if (a.protocol.equals(r.protocol) && a.hostPort == r.hostPort && eq(a.hostIp, r.hostIp)) + return true; + return false; + } + private static boolean eq(@Nullable String a, @Nullable String b) { return a == null ? b == null : a.equals(b); } + /** Reconcile/wait loop; stays alive the whole RUNNING period so late rule changes are still reconciled. */ private void loop() { - var rules = parseRules(); - if (rules.isEmpty()) return; - int attempts = 0; - while (running && vm.getState() == VMState.RUNNING) { - boolean allDone = true; - for (var rule : rules) { - if (rule.done) continue; - if (!running) return; - var target = resolveTarget(rule); - if (target == null) { - allDone = false; - continue; - } - boolean ok = networkStore.firewall.applyForward( - target.bridge, target.ip, rule.protocol, - rule.hostIp, rule.hostPort, rule.guestPort); - rule.done = true; - if (!ok) { - Log.w(TAG, fmt("VM %s: failed to apply forward %s :%d -> %s:%d", - vm.getName(), rule.protocol, rule.hostPort, target.ip, rule.guestPort)); - continue; - } - // 与 stop()/removeAll() 通过 applied 锁互斥,避免 stop 后仍残留规则 - synchronized (applied) { - if (!running) { - networkStore.firewall.removeForward( - target.bridge, target.ip, rule.protocol, - rule.hostIp, rule.hostPort, rule.guestPort); + try { + int attempts = 0; + synchronized (this) { + while (running && vm.getState() == VMState.RUNNING) { + boolean hasUnresolved = reconcile(); + if (!running) break; + if (hasUnresolved && attempts < RESOLVE_MAX_ATTEMPTS) { + attempts++; + wait(RESOLVE_INTERVAL_MS); } else { - applied.add(new Applied(target.bridge, target.ip, rule.protocol, - rule.hostIp, rule.hostPort, rule.guestPort)); - Log.i(TAG, fmt("VM %s: forward %s :%d -> %s:%d", - vm.getName(), rule.protocol, rule.hostPort, target.ip, rule.guestPort)); + if (hasUnresolved) + Log.w(TAG, fmt("VM %s: gave up auto-resolving guest IP for some port" + + " forwards after %d attempts; will retry on next config change", + vm.getName(), attempts)); + attempts = 0; + wait(); } } } - if (allDone) break; - if (++attempts >= RESOLVE_MAX_ATTEMPTS) { - Log.w(TAG, fmt("VM %s: gave up resolving guest IP for some port forwards after %d attempts", - vm.getName(), attempts)); - break; - } - threadSleep(RESOLVE_INTERVAL_MS); + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); } } @@ -263,7 +241,6 @@ private List parseRules() { if (guestPort <= 0 || guestPort > 65535) guestPort = hostPort; var hostIp = r.optString("host_ip", ""); if (hostIp == null) hostIp = ""; - // 同一 VM 内按 (protocol, host_ip, host_port) 去重,避免重复 DNAT var key = fmt("%s|%s|%d", protocol, hostIp, hostPort); if (!seen.add(key)) { Log.w(TAG, fmt("VM %s: duplicate port forward %s, skipping", vm.getName(), key)); @@ -304,10 +281,8 @@ private Target resolveTarget(@NonNull Rule rule) { private String resolveGuestIpByMac( @NonNull NetworkInstance netInst, @NonNull String bridge, @NonNull String mac) { var macLower = mac.toLowerCase(Locale.ROOT); - // 1. 优先用 dnsmasq DHCP 租约 var ip = matchMac(networkStore.backend.listDhcpLeases(bridge), macLower, "ip", "mac"); if (ip != null) return ip; - // 2. 回退到 ARP 邻居表(适用于静态 IP 且已通信过的 guest) return matchMac(netInst.listNeighbors(), macLower, "dst", "lladdr"); } @@ -370,7 +345,6 @@ private static final class Rule { int guestPort; String networkId; String fixedGuestIp; - boolean done = false; } private static final class Target { From 344e46956a0852f156a65745858ff355da8ab55c Mon Sep 17 00:00:00 2001 From: lateautumn233 Date: Wed, 20 May 2026 18:41:50 +0800 Subject: [PATCH 004/182] feat(daemon): create run directory with proper ownership on daemon start --- .../java/cn/classfun/droidvm/lib/daemon/DaemonHelper.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/cn/classfun/droidvm/lib/daemon/DaemonHelper.java b/app/src/main/java/cn/classfun/droidvm/lib/daemon/DaemonHelper.java index 34476a84e..735492673 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/daemon/DaemonHelper.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/daemon/DaemonHelper.java @@ -19,6 +19,7 @@ import android.os.Handler; import android.os.Looper; +import android.os.Process; import android.util.Log; import android.widget.Toast; @@ -100,7 +101,11 @@ public static String readToken() { public boolean startDaemon() { var zipPath = getDaemonZipPath(); Log.i(TAG, fmt("Starting daemon via app_process64 with %s", zipPath)); - run("mkdir -p %s", pathJoin(DATA_DIR, "run")); + var runDir = pathJoin(DATA_DIR, "run"); + var uid = Process.myUid(); + run("mkdir -p %s", runDir); + // Best-effort ownership fix; chown may be denied on some contexts (e.g. Android). + run("chown %d:%d %s || true", uid, uid, runDir); var cmd = fmt( "env CLASSPATH=%s %s %s / %s", escapedString(zipPath), From 7643cb527bb1cb716a26d1a805f3e2981d1527e1 Mon Sep 17 00:00:00 2001 From: lateautumn233 Date: Wed, 10 Jun 2026 19:44:18 +0800 Subject: [PATCH 005/182] feat(agent): disable mthp lending for agent vm - Set prepare_lend_mthp to false in default AgentVM config --- app/src/main/java/cn/classfun/droidvm/ui/agent/base/AgentVM.java | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/cn/classfun/droidvm/ui/agent/base/AgentVM.java b/app/src/main/java/cn/classfun/droidvm/ui/agent/base/AgentVM.java index 883c9d09b..94862a03f 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/agent/base/AgentVM.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/agent/base/AgentVM.java @@ -107,6 +107,7 @@ public VMConfig buildVM() { vm.item.set("temporary", true); vm.item.set("cpu_count", 1); vm.item.set("memory_mb", 384); + vm.item.set("prepare_lend_mthp", false); vm.item.set("use_uefi", false); vm.item.set("kernel", PATH_BUILTIN_KERNEL); vm.item.set("initrd", PATH_BUILTIN_INITRD); From c28c9b073716ddb54917dbca770bc8db28af84eb Mon Sep 17 00:00:00 2001 From: lateautumn233 Date: Thu, 21 May 2026 03:00:09 +0800 Subject: [PATCH 006/182] feat(settings): add option to clear logs before starting VM --- .../droidvm/daemon/console/ConsoleStream.java | 17 +++++++++-------- .../droidvm/daemon/ipc/vm/StartHandler.java | 4 ++++ .../classfun/droidvm/daemon/vm/VMInstance.java | 5 +++++ .../ui/main/settings/MainSettingsFragment.java | 9 +++++++++ .../cn/classfun/droidvm/ui/vm/VMActions.java | 2 ++ .../main/res/layout/fragment_main_settings.xml | 8 ++++++++ app/src/main/res/values-zh-rCN/strings.xml | 2 ++ app/src/main/res/values/strings.xml | 2 ++ 8 files changed, 41 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/console/ConsoleStream.java b/app/src/main/java/cn/classfun/droidvm/daemon/console/ConsoleStream.java index f4c7dcfef..023fbe257 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/console/ConsoleStream.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/console/ConsoleStream.java @@ -109,22 +109,23 @@ public void appendBuffer(@NonNull byte[] data, int off, int len) { public void clear() { buffer.clear(); + disableSave = false; if (logWriter != null) { try { logWriter.close(); } catch (Exception e) { Log.w(TAG, "Failed to close console log output stream", e); } - try { - var path = getPersistentPath(); - var file = new File(path); - if (file.exists() && !file.delete()) - Log.w(TAG, fmt("Failed to delete console log file %s", path)); - } catch (Exception e) { - Log.w(TAG, "Failed to delete console log file", e); - } logWriter = null; } + try { + var path = getPersistentPath(); + var file = new File(path); + if (file.exists() && !file.delete()) + Log.w(TAG, fmt("Failed to delete console log file %s", path)); + } catch (Exception e) { + Log.w(TAG, "Failed to delete console log file", e); + } } @NonNull diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/StartHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/StartHandler.java index 06c126125..97fad35a3 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/StartHandler.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/StartHandler.java @@ -9,6 +9,7 @@ import cn.classfun.droidvm.daemon.server.ClientRequest; import cn.classfun.droidvm.daemon.server.RequestException; import cn.classfun.droidvm.daemon.server.RequestHandler; +import cn.classfun.droidvm.lib.store.vm.VMState; @AutoService(RequestHandler.class) public final class StartHandler extends RequestHandler { @@ -28,6 +29,9 @@ public void handle(@NonNull ClientRequest request) throws Exception { var inst = vms.findById(vmId); if (inst == null) throw new RequestException(fmt("VM not found: %s", vmId)); + if (params.optBoolean("clear_logs_before_start", false) && + inst.getState() == VMState.STOPPED) + inst.clearLogs(); if (!inst.start()) throw new RequestException("failed to start VM"); } diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMInstance.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMInstance.java index 114250ea3..d8750b0fd 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMInstance.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMInstance.java @@ -305,6 +305,11 @@ public void joinThreads(long timeoutMs) { } } + public void clearLogs() { + for (var stream : getStreams()) + stream.clear(); + } + private void runVM() { var inst = getBackendInstance(); var vmId = getId().toString(); diff --git a/app/src/main/java/cn/classfun/droidvm/ui/main/settings/MainSettingsFragment.java b/app/src/main/java/cn/classfun/droidvm/ui/main/settings/MainSettingsFragment.java index d2cefe9ad..9f5cb65b6 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/main/settings/MainSettingsFragment.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/main/settings/MainSettingsFragment.java @@ -45,6 +45,7 @@ public final class MainSettingsFragment extends MainBaseFragment { private static final long DAEMON_REFRESH_INTERVAL_MS = 1000L; private static final String PREFS_NAME = "droidvm_prefs"; public static final String KEY_VM_AUTO_CONSOLE = "vm_auto_console"; + public static final String KEY_VM_CLEAR_LOGS_BEFORE_START = "vm_clear_logs_before_start"; public static final String KEY_AUTO_CHECK_UPDATE = "auto_check_update"; private final Handler mainHandler = new Handler(Looper.getMainLooper()); private final Runnable daemonStatusRefreshRunnable = this::periodRefreshDaemonStatus; @@ -57,6 +58,7 @@ public final class MainSettingsFragment extends MainBaseFragment { private TextRowWidget itemDaemonStop; private TextRowWidget itemDaemonRestart; private SwitchRowWidget itemVMAutoConsole; + private SwitchRowWidget itemVMClearLogsBeforeStart; private TextRowWidget itemLicense; private SwitchRowWidget itemAutoCheckUpdate; private TextRowWidget itemCheckUpdate; @@ -104,6 +106,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat itemDaemonStop = view.findViewById(R.id.item_daemon_stop); itemDaemonRestart = view.findViewById(R.id.item_daemon_restart); itemVMAutoConsole = view.findViewById(R.id.item_vm_auto_console); + itemVMClearLogsBeforeStart = view.findViewById(R.id.item_vm_clear_logs_before_start); itemLicense = view.findViewById(R.id.item_license); itemAutoCheckUpdate = view.findViewById(R.id.item_auto_check_update); itemCheckUpdate = view.findViewById(R.id.item_check_update); @@ -123,6 +126,7 @@ private void initSettings() { bindOnClick(itemDaemonStop, daemon::asyncStopDaemon); bindOnClick(itemDaemonRestart, daemon::asyncRestartDaemon); bindOnChecked(itemVMAutoConsole, KEY_VM_AUTO_CONSOLE, false); + bindOnChecked(itemVMClearLogsBeforeStart, KEY_VM_CLEAR_LOGS_BEFORE_START, false); bindOnChecked(itemAutoCheckUpdate, KEY_AUTO_CHECK_UPDATE, true); bindOnClick(itemCheckUpdate, this::checkUpdate); bindOnClick(itemPrivacy, this::showPrivacyPolicy); @@ -159,6 +163,11 @@ public static boolean isAutoConsoleEnabled(@NonNull Context context) { return prefs.getBoolean(KEY_VM_AUTO_CONSOLE, false); } + public static boolean isClearLogsBeforeStartEnabled(@NonNull Context context) { + var prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + return prefs.getBoolean(KEY_VM_CLEAR_LOGS_BEFORE_START, false); + } + public static boolean isAutoCheckUpdateEnabled(@NonNull Context context) { var prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); return prefs.getBoolean(KEY_AUTO_CHECK_UPDATE, true); diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/VMActions.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/VMActions.java index 1259a87fa..006db820d 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/VMActions.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/VMActions.java @@ -2,6 +2,7 @@ import static android.widget.Toast.LENGTH_LONG; import static cn.classfun.droidvm.ui.main.settings.MainSettingsFragment.isAutoConsoleEnabled; +import static cn.classfun.droidvm.ui.main.settings.MainSettingsFragment.isClearLogsBeforeStartEnabled; import android.os.Handler; import android.util.Log; @@ -44,6 +45,7 @@ public static void createAndStart( DaemonConnection.OnResponse onCreateModify = resp -> conn .buildRequest("vm_start") .copy(resp, "vm_id") + .put("clear_logs_before_start", isClearLogsBeforeStartEnabled(ui.getContext())) .onResponse(onStart) .onUnsuccessful(f) .onError(err) diff --git a/app/src/main/res/layout/fragment_main_settings.xml b/app/src/main/res/layout/fragment_main_settings.xml index fc86e16c8..94c6effc8 100644 --- a/app/src/main/res/layout/fragment_main_settings.xml +++ b/app/src/main/res/layout/fragment_main_settings.xml @@ -55,6 +55,14 @@ android:subtitle="@string/settings_vm_auto_console_summary" android:text="@string/settings_vm_auto_console_title" /> + + 虚拟机
启动时打开控制台 启动虚拟机时自动打开控制台 + 启动前清理日志 + 启动虚拟机前清理该虚拟机的所有控制台日志 关于 问题反馈 在 GitHub 上报告问题 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1d0e83c89..a8ef6579b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -295,6 +295,8 @@ Virtual Machine Open console on start Automatically open the console when a VM is started + Clear logs before start + Clear all console logs for the VM before it starts About Feedback Report issues on GitHub From b55f59f46ef6b0538908c526c623c4bdf1470ede Mon Sep 17 00:00:00 2001 From: lateautumn233 Date: Thu, 11 Jun 2026 22:54:41 +0800 Subject: [PATCH 007/182] chore(ci): skip re-compression when uploading artifacts - bump actions/upload-artifact to v7 - set archive: false to avoid re-zipping already-compressed apk --- .github/workflows/build.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 48f253d69..24ba99086 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -76,13 +76,15 @@ jobs: echo "name=${ver}" >> "$GITHUB_OUTPUT" - name: Upload debug APK - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: DroidVM-${{ steps.version.outputs.name }}-debug path: app/build/outputs/apk/debug/*.apk + archive: false - name: Upload release APK - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: DroidVM-${{ steps.version.outputs.name }}-release path: app/build/outputs/apk/release/*.apk + archive: false From 67aeb0fee28eea02c8c1d794799f6b232900bf02 Mon Sep 17 00:00:00 2001 From: lateautumn233 Date: Fri, 12 Jun 2026 02:41:36 +0800 Subject: [PATCH 008/182] feat(logs): add share functionality - Added ShareUtils for exporting logs to system share sheet via FileProvider - Integrated share option in Logs and VM Console menus - Configured FileProvider with cache directory for safe file sharing - Added bilingual string resources for share actions - Refactored VMConsoleActivity to extract console text fetching logic --- app/src/main/AndroidManifest.xml | 10 ++ .../droidvm/lib/utils/ShareUtils.java | 93 +++++++++++++++++++ .../droidvm/ui/logs/LogsActivity.java | 32 ++++++- .../ui/vm/console/VMConsoleActivity.java | 58 +++++++++--- app/src/main/res/drawable/ic_share.xml | 9 ++ app/src/main/res/menu/menu_logs.xml | 6 ++ app/src/main/res/menu/menu_vm_console.xml | 5 + app/src/main/res/values-zh-rCN/strings.xml | 3 + app/src/main/res/values/strings.xml | 3 + app/src/main/res/xml/file_paths.xml | 7 ++ 10 files changed, 209 insertions(+), 17 deletions(-) create mode 100644 app/src/main/java/cn/classfun/droidvm/lib/utils/ShareUtils.java create mode 100644 app/src/main/res/drawable/ic_share.xml create mode 100644 app/src/main/res/xml/file_paths.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b1a30d734..dcdd950ce 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -166,5 +166,15 @@ android:theme="@style/Theme.DroidVM.NoActionBar" android:windowSoftInputMode="adjustResize" /> + + + + diff --git a/app/src/main/java/cn/classfun/droidvm/lib/utils/ShareUtils.java b/app/src/main/java/cn/classfun/droidvm/lib/utils/ShareUtils.java new file mode 100644 index 000000000..66d28397b --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/lib/utils/ShareUtils.java @@ -0,0 +1,93 @@ +package cn.classfun.droidvm.lib.utils; + +import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; +import static cn.classfun.droidvm.lib.utils.ThreadUtils.runOnPool; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; + +import androidx.core.content.FileProvider; + +import java.io.File; +import java.io.FileOutputStream; +import java.nio.charset.StandardCharsets; + +/** Helpers for exporting in-app text to the system share sheet (ACTION_SEND). */ +public final class ShareUtils { + private static final String TAG = "ShareUtils"; + private static final String SHARE_DIR = "shared_logs"; + private static final Handler MAIN = new Handler(Looper.getMainLooper()); + + private ShareUtils() { + } + + /** + * Write {@code content} to a temp file under the cache dir and launch the system + * share sheet with it as a {@code text/plain} attachment. File write runs off the + * main thread; the chooser is launched back on the main thread. {@code onError} is + * invoked on the main thread with a message when sharing fails. + */ + public static void shareTextAsFile( + Context context, + String filename, + String content, + String chooserTitle, + OnError onError + ) { + var appContext = context.getApplicationContext(); + runOnPool(() -> { + try { + var dir = new File(appContext.getCacheDir(), SHARE_DIR); + if (!dir.exists() && !dir.mkdirs()) + throw new Exception("Failed to create share dir"); + pruneOldFiles(dir); + var file = new File(dir, filename); + try (var os = new FileOutputStream(file)) { + os.write(content.getBytes(StandardCharsets.UTF_8)); + os.flush(); + } + var authority = fmt("%s.fileprovider", appContext.getPackageName()); + var uri = FileProvider.getUriForFile(appContext, authority, file); + MAIN.post(() -> launchChooser(context, uri, chooserTitle, onError)); + } catch (Exception e) { + Log.e(TAG, "Failed to prepare shared file", e); + MAIN.post(() -> { + if (onError != null) onError.onError(e.getMessage()); + }); + } + }); + } + + private static void launchChooser(Context context, Uri uri, String title, OnError onError) { + try { + var send = new Intent(Intent.ACTION_SEND) + .setType("text/plain") + .putExtra(Intent.EXTRA_STREAM, uri) + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + var chooser = Intent.createChooser(send, title); + chooser.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + context.startActivity(chooser); + } catch (Exception e) { + Log.e(TAG, "Failed to launch share chooser", e); + if (onError != null) onError.onError(e.getMessage()); + } + } + + /** Drop previously exported files so the cache dir does not grow unbounded. */ + private static void pruneOldFiles(File dir) { + var files = dir.listFiles(); + if (files == null) return; + for (var f : files) { + //noinspection ResultOfMethodCallIgnored + f.delete(); + } + } + + public interface OnError { + void onError(String message); + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/ui/logs/LogsActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/logs/LogsActivity.java index eeff23798..206cd9b76 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/logs/LogsActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/logs/LogsActivity.java @@ -32,6 +32,7 @@ import cn.classfun.droidvm.R; import cn.classfun.droidvm.lib.daemon.DaemonHelper; +import cn.classfun.droidvm.lib.utils.ShareUtils; public final class LogsActivity extends AppCompatActivity { private static final int MAX_LOG_LINES = 10000; @@ -88,6 +89,10 @@ private boolean onMenuItemClick(@NonNull MenuItem item) { saveLogs(); return true; } + if (id == R.id.menu_share) { + shareLogs(); + return true; + } if (id == R.id.menu_scroll_bottom) { scrollToBottom(); return true; @@ -99,10 +104,31 @@ private boolean onMenuItemClick(@NonNull MenuItem item) { return false; } - private void saveLogs() { + private String logFilename() { var sdf = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()); - var filename = fmt("droidvm_logs_%s.txt", sdf.format(new Date())); - saveLauncher.launch(filename); + return fmt("droidvm_logs_%s.txt", sdf.format(new Date())); + } + + private void saveLogs() { + saveLauncher.launch(logFilename()); + } + + private void shareLogs() { + var sb = new StringBuilder(); + for (var line : adapter.getLines()) { + sb.append(line).append('\n'); + } + ShareUtils.shareTextAsFile( + this, + logFilename(), + sb.toString(), + getString(R.string.logs_share_title), + msg -> Toast.makeText( + this, + fmt(getString(R.string.logs_share_failed), msg), + Toast.LENGTH_LONG + ).show() + ); } private void onSaveResult(Uri uri) { diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/console/VMConsoleActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/console/VMConsoleActivity.java index 3234178b8..c27fb9e52 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/console/VMConsoleActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/console/VMConsoleActivity.java @@ -50,6 +50,7 @@ import cn.classfun.droidvm.lib.daemon.DaemonConnection; import cn.classfun.droidvm.lib.ui.termux.SimpleTerminalSessionClient; import cn.classfun.droidvm.lib.ui.termux.SimpleTerminalViewClient; +import cn.classfun.droidvm.lib.utils.ShareUtils; public final class VMConsoleActivity extends AppCompatActivity { private static final String TAG = "VMConsoleActivity"; @@ -172,6 +173,9 @@ private boolean onMenuItemClicked(@NonNull MenuItem item) { if (id == R.id.action_save_log) { saveLogToFile(); return true; + } else if (id == R.id.action_share_log) { + shareLog(); + return true; } else if (id == R.id.action_clear_log) { clearLog(); return true; @@ -258,27 +262,18 @@ private void saveLogToFile() { )); } - private void onSaveLogResult(@Nullable Uri uri) { - if (uri == null) return; - Consumer showToast = resId -> runOnUiThread(() -> - Toast.makeText(this, resId, LENGTH_SHORT).show()); + /** Fetch the full console history via IPC; {@code onText} receives the decoded text. */ + private void fetchConsoleText(Consumer onText) { DaemonConnection.OnError err = e -> { Log.w(TAG, fmt("Failed to fetch log for %s stream %s", vmName, streamName), e); - showToast.accept(R.string.vm_info_logs_no_logs); + runOnUiThread(() -> + Toast.makeText(this, R.string.vm_info_logs_no_logs, LENGTH_SHORT).show()); }; DaemonConnection.OnUnsuccessful failed = resp -> err.onError(new Exception(resp.optString("message", "Unknown error"))); DaemonConnection.OnResponse success = resp -> { var data = resp.optString(streamName, ""); - var text = URLDecoder.decode(data, StandardCharsets.UTF_8); - try (var os = requireNonNull(getContentResolver().openOutputStream(uri))) { - os.write(text.getBytes(StandardCharsets.UTF_8)); - os.flush(); - showToast.accept(R.string.logs_save_success); - } catch (Exception e) { - Log.e(TAG, "Failed to save log file", e); - showToast.accept(R.string.vm_info_logs_save_failed); - } + onText.accept(URLDecoder.decode(data, StandardCharsets.UTF_8)); }; runOnPool(() -> DaemonConnection.getInstance().buildRequest("vm_console_history") .put("vm_id", vmId) @@ -289,6 +284,41 @@ private void onSaveLogResult(@Nullable Uri uri) { .invoke()); } + private void onSaveLogResult(@Nullable Uri uri) { + if (uri == null) return; + Consumer showToast = resId -> runOnUiThread(() -> + Toast.makeText(this, resId, LENGTH_SHORT).show()); + fetchConsoleText(text -> { + try (var os = requireNonNull(getContentResolver().openOutputStream(uri))) { + os.write(text.getBytes(StandardCharsets.UTF_8)); + os.flush(); + showToast.accept(R.string.logs_save_success); + } catch (Exception e) { + Log.e(TAG, "Failed to save log file", e); + showToast.accept(R.string.vm_info_logs_save_failed); + } + }); + } + + private void shareLog() { + var sdf = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()); + var filename = fmt( + "droidvm_console_%s_%s_%s.txt", + vmName, streamName, sdf.format(new Date()) + ); + fetchConsoleText(text -> ShareUtils.shareTextAsFile( + this, + filename, + text, + getString(R.string.logs_share_title), + msg -> runOnUiThread(() -> Toast.makeText( + this, + fmt(getString(R.string.logs_share_failed), msg), + Toast.LENGTH_LONG + ).show()) + )); + } + private void clearLog() { runOnPool(() -> DaemonConnection.getInstance().buildRequest("vm_console_clear") .put("vm_id", vmId) diff --git a/app/src/main/res/drawable/ic_share.xml b/app/src/main/res/drawable/ic_share.xml new file mode 100644 index 000000000..c423dd951 --- /dev/null +++ b/app/src/main/res/drawable/ic_share.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/menu/menu_logs.xml b/app/src/main/res/menu/menu_logs.xml index d15b80881..e8630f50c 100644 --- a/app/src/main/res/menu/menu_logs.xml +++ b/app/src/main/res/menu/menu_logs.xml @@ -8,6 +8,12 @@ android:title="@string/logs_save" app:showAsAction="ifRoom" /> + + + + 保存 日志保存成功 保存日志失败:%1$s + 分享 + 分享日志 + 分享日志失败:%1$s 隐私政策 继续之前请阅读并同意隐私政策 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a8ef6579b..67e7b2cd5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -776,6 +776,9 @@ Save Logs saved successfully Failed to save logs: %1$s + Share + Share logs + Failed to share logs: %1$s Privacy Policy Please read and agree to the privacy policy before continuing diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml new file mode 100644 index 000000000..fa1e5caaa --- /dev/null +++ b/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,7 @@ + + + + + From 033ea83104b23605e33d808dd160d36e12b148a1 Mon Sep 17 00:00:00 2001 From: HuJK Date: Sun, 14 Jun 2026 16:31:00 +0800 Subject: [PATCH 009/182] rework(network): introduce bridge backends and VLAN networking Rework VM networking around typed network and NIC config, Linux/gVisor bridge backends, VLAN-aware L2/L3 behavior, bridgedhcp PD/DHCP service integration, pbridge/gvswitch tooling, and per-NIC static leases/port forwards. Add the netbox helper and focused network/store tests, and move the old VM-level port-forward editor into NIC/network configuration. Co-authored-by: KusakabeShi Co-authored-by: Claude Fable 5 --- app/src/main/AndroidManifest.xml | 7 + .../daemon/ipc/network/InfoHandler.java | 7 +- .../ipc/network/ListUplinksHandler.java | 24 + .../daemon/ipc/network/PdReleaseHandler.java | 36 + .../daemon/ipc/network/PdRenewHandler.java | 36 + .../daemon/ipc/network/StopHandler.java | 3 + .../daemon/ipc/network/ToolLogHandler.java | 41 + .../daemon/ipc/vm/PortForwardListHandler.java | 37 - .../daemon/ipc/vm/PortForwardSetHandler.java | 41 - .../daemon/network/NetworkInstance.java | 255 +++-- .../daemon/network/NetworkInstanceStore.java | 28 +- .../daemon/network/backend/BackendBase.java | 2 - .../daemon/network/backend/BridgeBackend.java | 147 +++ .../daemon/network/backend/BridgeDhcp.java | 490 +++++++++ .../network/backend/DefaultRouterWatcher.java | 270 ++++- .../daemon/network/backend/Dnsmasq.java | 217 ---- .../network/backend/FirewallHelper.java | 38 +- .../network/backend/LinuxBridgeBackend.java | 751 ++++++++++++++ .../daemon/network/backend/LinuxNetwork.java | 415 ++++---- .../network/backend/ManagedProcess.java | 196 ++++ .../daemon/network/backend/Netbox.java | 279 +++++ .../daemon/network/backend/Pbridge.java | 94 ++ .../network/backend/UplinkResolver.java | 251 +++++ .../backend/gvisor/GvisorBridgeBackend.java | 518 ++++++++++ .../backend/gvisor/GvswitchClient.java | 213 ++++ .../backend/gvisor/GvswitchConfigBuilder.java | 123 +++ .../network/backend/iptables/ChainInfo.java | 56 +- .../backend/iptables/IptablesBackend.java | 157 ++- .../iptables/IptablesNetworkInstance.java | 129 ++- .../daemon/network/backend/pd/Duid.java | 50 + .../droidvm/daemon/server/ServerContext.java | 3 + .../droidvm/daemon/vm/VMInstance.java | 120 +-- .../droidvm/daemon/vm/VMPortForwarder.java | 359 ------- .../cn/classfun/droidvm/lib/Constants.java | 7 + .../droidvm/lib/network/IPv4Network.java | 13 + .../droidvm/lib/network/IPv6Network.java | 18 + .../droidvm/lib/store/network/BridgeType.java | 27 + .../droidvm/lib/store/network/Ipv6Source.java | 27 + .../lib/store/network/NetworkConfig.java | 125 +++ .../store/network/NetworkConfigValidator.java | 181 ++++ .../lib/store/network/NetworkStore.java | 22 + .../droidvm/lib/store/network/UplinkMode.java | 28 + .../droidvm/lib/store/network/VlanConfig.java | 235 +++++ .../droidvm/lib/store/vm/VMConfig.java | 11 + .../droidvm/lib/store/vm/VMNicConfig.java | 322 ++++++ .../classfun/droidvm/lib/utils/NetUtils.java | 4 + .../ui/main/network/NetworkAdapter.java | 24 +- .../ui/network/edit/NetworkEditActivity.java | 957 +++++++++--------- .../ui/network/edit/VlanCardBinder.java | 237 +++++ .../ui/network/info/NetworkInfoActivity.java | 325 +++++- .../network/info/NetworkToolLogActivity.java | 94 ++ .../droidvm/ui/vm/edit/VMEditActivity.java | 5 + .../droidvm/ui/vm/edit/base/VMEditTab.java | 6 - .../ui/vm/edit/network/VMNetEditAdapter.java | 353 ++++++- .../vm/edit/network/VMNetEditViewHolder.java | 45 + .../portforward/PortForwardEditAdapter.java | 174 ---- .../PortForwardEditViewHolder.java | 66 -- .../portforward/VMEditPortForwardTab.java | 66 -- .../droidvm/ui/vm/info/VMInfoActivity.java | 144 +-- .../ui/widgets/row/SwitchRowWidget.java | 13 + .../main/res/layout/activity_network_edit.xml | 189 ++-- .../res/layout/activity_network_tool_log.xml | 40 + app/src/main/res/layout/activity_vm_edit.xml | 7 - app/src/main/res/layout/activity_vm_info.xml | 11 - .../res/layout/dialog_port_forward_edit.xml | 16 - app/src/main/res/layout/item_network_pd.xml | 71 ++ app/src/main/res/layout/item_network_vlan.xml | 367 +++++++ .../res/layout/item_port_forward_edit.xml | 210 ---- app/src/main/res/layout/item_vm_net_edit.xml | 220 ++++ .../layout/partial_vm_edit_port_forward.xml | 22 - app/src/main/res/values-zh-rCN/strings.xml | 99 +- app/src/main/res/values/strings.xml | 99 +- app/src/main/rust/netbox/.gitignore | 2 + app/src/main/rust/netbox/Cargo.lock | 574 +++++++++++ app/src/main/rust/netbox/Cargo.toml | 27 + app/src/main/rust/netbox/build.sh | 40 + app/src/main/rust/netbox/src/main.rs | 263 +++++ app/src/main/rust/netbox/src/nl.rs | 762 ++++++++++++++ app/src/main/rust/netbox/src/tap.rs | 48 + .../lib/network/IPNetworkOffsetTest.java | 67 ++ .../network/NetworkConfigValidatorTest.java | 162 +++ .../lib/store/network/VlanConfigPdTest.java | 57 ++ .../droidvm/lib/store/vm/PortForwardTest.java | 82 ++ 83 files changed, 9838 insertions(+), 2519 deletions(-) create mode 100644 app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/ListUplinksHandler.java create mode 100644 app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/PdReleaseHandler.java create mode 100644 app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/PdRenewHandler.java create mode 100644 app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/ToolLogHandler.java delete mode 100644 app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/PortForwardListHandler.java delete mode 100644 app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/PortForwardSetHandler.java create mode 100644 app/src/main/java/cn/classfun/droidvm/daemon/network/backend/BridgeBackend.java create mode 100644 app/src/main/java/cn/classfun/droidvm/daemon/network/backend/BridgeDhcp.java delete mode 100644 app/src/main/java/cn/classfun/droidvm/daemon/network/backend/Dnsmasq.java create mode 100644 app/src/main/java/cn/classfun/droidvm/daemon/network/backend/LinuxBridgeBackend.java create mode 100644 app/src/main/java/cn/classfun/droidvm/daemon/network/backend/ManagedProcess.java create mode 100644 app/src/main/java/cn/classfun/droidvm/daemon/network/backend/Netbox.java create mode 100644 app/src/main/java/cn/classfun/droidvm/daemon/network/backend/Pbridge.java create mode 100644 app/src/main/java/cn/classfun/droidvm/daemon/network/backend/UplinkResolver.java create mode 100644 app/src/main/java/cn/classfun/droidvm/daemon/network/backend/gvisor/GvisorBridgeBackend.java create mode 100644 app/src/main/java/cn/classfun/droidvm/daemon/network/backend/gvisor/GvswitchClient.java create mode 100644 app/src/main/java/cn/classfun/droidvm/daemon/network/backend/gvisor/GvswitchConfigBuilder.java create mode 100644 app/src/main/java/cn/classfun/droidvm/daemon/network/backend/pd/Duid.java delete mode 100644 app/src/main/java/cn/classfun/droidvm/daemon/vm/VMPortForwarder.java create mode 100644 app/src/main/java/cn/classfun/droidvm/lib/store/network/BridgeType.java create mode 100644 app/src/main/java/cn/classfun/droidvm/lib/store/network/Ipv6Source.java create mode 100644 app/src/main/java/cn/classfun/droidvm/lib/store/network/NetworkConfigValidator.java create mode 100644 app/src/main/java/cn/classfun/droidvm/lib/store/network/UplinkMode.java create mode 100644 app/src/main/java/cn/classfun/droidvm/lib/store/network/VlanConfig.java create mode 100644 app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMNicConfig.java create mode 100644 app/src/main/java/cn/classfun/droidvm/ui/network/edit/VlanCardBinder.java create mode 100644 app/src/main/java/cn/classfun/droidvm/ui/network/info/NetworkToolLogActivity.java delete mode 100644 app/src/main/java/cn/classfun/droidvm/ui/vm/edit/portforward/PortForwardEditAdapter.java delete mode 100644 app/src/main/java/cn/classfun/droidvm/ui/vm/edit/portforward/PortForwardEditViewHolder.java delete mode 100644 app/src/main/java/cn/classfun/droidvm/ui/vm/edit/portforward/VMEditPortForwardTab.java create mode 100644 app/src/main/res/layout/activity_network_tool_log.xml delete mode 100644 app/src/main/res/layout/dialog_port_forward_edit.xml create mode 100644 app/src/main/res/layout/item_network_pd.xml create mode 100644 app/src/main/res/layout/item_network_vlan.xml delete mode 100644 app/src/main/res/layout/item_port_forward_edit.xml delete mode 100644 app/src/main/res/layout/partial_vm_edit_port_forward.xml create mode 100644 app/src/main/rust/netbox/.gitignore create mode 100644 app/src/main/rust/netbox/Cargo.lock create mode 100644 app/src/main/rust/netbox/Cargo.toml create mode 100755 app/src/main/rust/netbox/build.sh create mode 100644 app/src/main/rust/netbox/src/main.rs create mode 100644 app/src/main/rust/netbox/src/nl.rs create mode 100644 app/src/main/rust/netbox/src/tap.rs create mode 100644 app/src/test/java/cn/classfun/droidvm/lib/network/IPNetworkOffsetTest.java create mode 100644 app/src/test/java/cn/classfun/droidvm/lib/store/network/NetworkConfigValidatorTest.java create mode 100644 app/src/test/java/cn/classfun/droidvm/lib/store/network/VlanConfigPdTest.java create mode 100644 app/src/test/java/cn/classfun/droidvm/lib/store/vm/PortForwardTest.java diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index dcdd950ce..ffe9d0410 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -152,6 +152,13 @@ android:theme="@style/Theme.DroidVM.NoActionBar" android:windowSoftInputMode="adjustResize" /> + + { - try { - var addr = IPv4Network.parse(a.asString()); - if (!store.backend.addAddress(br, addr)) - Log.w(TAG, fmt("Failed to add IPv4 %s to %s", addr, br)); - } catch (Exception ignored) { - } - }); - item.get("ipv6_addresses").forEachArray(a -> { - try { - var addr = IPv6Network.parse(a.asString()); - if (!store.backend.addAddress(br, addr)) - Log.w(TAG, fmt("Failed to add IPv6 %s to %s", addr, br)); - } catch (Exception ignored) { - } - }); - if (!store.backend.setLinkState(br, true)) - throw new RuntimeException(fmt("Failed to bring up bridge %s", br)); - store.firewall.initNetwork(this); - store.backend.populateRuleRoute(this); - store.context.getRouterWatcher().setForNewNetwork(this); - var dhcpEnabled = item.optBoolean("dhcp_enabled", false); - var dhcpStart = item.optString("dhcp_range_start", ""); - var dhcpEnd = item.optString("dhcp_range_end", ""); - if (dhcpEnabled && !dhcpStart.isEmpty() && !dhcpEnd.isEmpty()) { - if (dnsmasq == null) dnsmasq = new Dnsmasq(this); - dnsmasq.startDnsmasq(); - } + backend = createBackend(); + backend.start(); state = NetworkState.RUNNING; - Log.i(TAG, fmt("Network %s started on bridge %s", getName(), br)); + Log.i(TAG, fmt( + "Network %s started on bridge %s", getName(), getBridgeName() + )); return true; } catch (Exception e) { Log.e(TAG, fmt("Failed to start network %s", getName()), e); + if (backend != null) { + try { + backend.stop(); + } catch (Exception cleanup) { + Log.w(TAG, "Cleanup after failed start failed", cleanup); + } + backend = null; + } state = NetworkState.STOPPED; - store.backend.deleteBridge(br); return false; } } public boolean stop() { + return stop(false); + } + + public boolean stop(boolean force) { if (state != NetworkState.RUNNING) { Log.w(TAG, fmt("Network %s not running, cannot stop", getName())); return false; } + if (!force) { + var vm = findRunningVMUsing(); + if (vm != null) { + Log.w(TAG, fmt( + "Network %s is in use by running VM %s, cannot stop", + getName(), vm + )); + return false; + } + } state = NetworkState.STOPPING; - var br = item.optString("bridge_name", ""); - if (dnsmasq != null) - dnsmasq.stopDnsmasq(); - store.firewall.deinitNetwork(this); - store.backend.setLinkState(br, false); - if (!store.backend.deleteBridge(br)) - Log.w(TAG, fmt("Failed to delete bridge %s", br)); + if (backend != null) { + backend.stop(); + backend = null; + } state = NetworkState.STOPPED; Log.i(TAG, fmt("Network %s stopped", getName())); return true; } - public boolean addAddress(@NonNull IPNetwork cidr) { - if (!store.backend.addAddress(item.optString("bridge_name", ""), cidr)) return false; - var key = (cidr instanceof IPv6Network) ? "ipv6_addresses" : "ipv4_addresses"; - var arr = item.get(key); - if (arr.is(DataItem.Type.NULL)) { - arr = DataItem.newArray(); - item.set(key, arr); + /** Name of a non-stopped VM with a NIC on this network, or null. */ + @Nullable + public String findRunningVMUsing() { + var vms = store.context.getVMs(); + if (vms == null) return null; + var netId = getId().toString(); + var found = new String[1]; + vms.forEach((vmId, vm) -> { + if (found[0] != null) return; + if (vm.getState() == VMState.STOPPED) return; + vm.forEachNic(nic -> { + if (netId.equals(nic.getNetworkId())) + found[0] = vm.getName(); + }); + }); + return found[0]; + } + + public void attachNic(@NonNull VMNicConfig nic, @NonNull String tapName) throws Exception { + if (state != NetworkState.RUNNING || backend == null) + throw new IllegalStateException(fmt("Network %s is not running", getName())); + // The NIC may carry L3-only options (DHCP static lease / port forwards) + // kept in its saved config for when it moves back to an L3 network. + // Strip the ones this network can't honour into a throwaway copy so the + // VM still boots; the stored config is untouched, and genuine + // misconfigurations still fail validation below. + var dropped = new java.util.ArrayList(); + var effective = nic.sanitizedFor(this, dropped); + if (!dropped.isEmpty()) + Log.i(TAG, fmt("NIC %s on network %s: ignoring unsupported options: %s", + tapName, getName(), String.join(", ", dropped))); + effective.validate(this); + backend.attachNic(effective, tapName); + } + + public void detachNic(@NonNull VMNicConfig nic, @NonNull String tapName) { + if (backend == null) return; + try { + backend.detachNic(nic, tapName); + } catch (Exception e) { + Log.w(TAG, fmt("Failed to detach NIC %s", tapName), e); } - arr.append(DataItem.newString(cidr.toString())); - return true; + } + + @NonNull + public Map getLiveV6Networks() { + return backend != null ? backend.getLiveV6Networks() : Map.of(); + } + + /** Runtime-only address tools used by the info screen (Linux only). */ + public boolean addAddress(@NonNull IPNetwork cidr) { + if (getBridgeType() != BridgeType.LINUX || state != NetworkState.RUNNING) + return false; + return store.backend.addAddress(item.optString("bridge_name", ""), cidr); } public boolean removeAddress(@NonNull IPNetwork cidr) { + if (getBridgeType() != BridgeType.LINUX || state != NetworkState.RUNNING) + return false; return store.backend.removeAddress(item.optString("bridge_name", ""), cidr); } public boolean addInterface(@NonNull String ifname) { + if (getBridgeType() != BridgeType.LINUX || state != NetworkState.RUNNING) + return false; return store.backend.addInterface(item.optString("bridge_name", ""), ifname); } public boolean removeInterface(@NonNull String ifname) { + if (getBridgeType() != BridgeType.LINUX) return false; return store.backend.removeInterface(ifname); } public JSONArray listAddresses() { - return store.backend.listAddresses(item.optString("bridge_name", "")); + if (backend == null) return new JSONArray(); + return backend.listAddresses(); + } + + /** Three-dimension address rows (VLAN/bound/source), or null if unsupported. */ + @Nullable + public JSONArray listAddressEntries() { + return backend == null ? null : backend.listAddressEntries(); + } + + /** Per-VLAN DHCPv6-PD client state. */ + public JSONArray listPdStatus() { + if (backend == null) return new JSONArray(); + return backend.listPdStatus(); + } + + /** Forces a PD renew on one VLAN's client. */ + public boolean renewPd(int vlanId) { + return backend != null && backend.renewPd(vlanId); + } + + /** Releases and re-solicits one VLAN's PD delegation. */ + public boolean releasePd(int vlanId) { + return backend != null && backend.releasePd(vlanId); + } + + /** Helper tools this network runs, as [{key, running}]. */ + public JSONArray listTools() { + if (backend == null) return new JSONArray(); + return backend.listTools(); + } + + /** Captured stdout/stderr of a named tool, or null when unknown. */ + @Nullable + public java.util.List toolLog(@NonNull String key) { + return backend == null ? null : backend.toolLog(key); } public JSONArray listBridges() { @@ -179,24 +258,50 @@ public JSONArray listAvailableInterfaces() { } public JSONArray listInterfaces() { - var vms = store.context.getVMs(); - return store.backend.listInterfaces(vms, item.optString("bridge_name", "")); + if (backend == null) return new JSONArray(); + return backend.listInterfaces(store.context.getVMs()); } public JSONArray listNeighbors() { - return store.backend.listNeighbors(item.optString("bridge_name", "")); + if (backend == null) return new JSONArray(); + return backend.listNeighbors(); } public JSONArray listDhcpLeases() { - return store.backend.listDhcpLeases(item.optString("bridge_name", "")); + if (backend == null) return new JSONArray(); + return backend.listDhcpLeases(); + } + + /** + * Devices the host routes for this network: the main bridge for the + * untagged VLAN-0 domain plus the per-VLAN bridges of the tagged + * VLANs (L3 mode only). + */ + @NonNull + public java.util.List getL3Devices() { + var out = new java.util.ArrayList(); + var br = getBridgeName(); + if (br == null) return out; + switch (getUplinkMode()) { + case L3: + for (var vlan : getVlans()) { + var dev = cn.classfun.droidvm.daemon.network.backend.LinuxNetwork + .vlanDevice(br, vlan.getVlanId()); + if (!out.contains(dev)) out.add(dev); + } + break; + default: + break; + } + return out; } @NonNull public JSONObject toInfoJson() throws JSONException { var obj = item.toJson(); obj.put("state", state.name().toLowerCase()); - obj.put("dnsmasq_running", isDnsmasqRunning()); - obj.put("dnsmasq_exit_code", getDnsmasqExitCode()); + if (backend != null) backend.appendInfo(obj); return obj; } + } diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/NetworkInstanceStore.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/NetworkInstanceStore.java index a3f31396e..1479bc7e6 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/network/NetworkInstanceStore.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/NetworkInstanceStore.java @@ -22,13 +22,15 @@ import cn.classfun.droidvm.daemon.network.backend.LinuxNetwork; import cn.classfun.droidvm.daemon.server.ServerContext; import cn.classfun.droidvm.lib.store.network.NetworkConfig; +import cn.classfun.droidvm.lib.store.network.NetworkConfigValidator; import cn.classfun.droidvm.lib.store.base.DataStore; +import cn.classfun.droidvm.lib.utils.JsonUtils; @SuppressWarnings("BooleanMethodIsAlwaysInverted") public final class NetworkInstanceStore extends DataStore { private static final String TAG = "NetworkInstanceStore"; public final FirewallHelper firewall = new IptablesBackend(); - public final BackendBase backend = new LinuxNetwork(); + public final LinuxNetwork backend = new LinuxNetwork(); public final ServerContext context; public NetworkInstanceStore(@NonNull ServerContext context) { @@ -37,6 +39,26 @@ public NetworkInstanceStore(@NonNull ServerContext context) { Log.i(TAG, "Network instance store initialized"); } + @Override + protected boolean load(@NonNull DataStore store, @NonNull JSONObject obj) { + try { + store.clear(); + JsonUtils.forEachArray(obj, getTypeName(), (JSONObject entry) -> { + if (!NetworkConfig.isSupportedSchema(entry)) { + Log.w(TAG, "Skipping network config with unsupported schema: " + + entry.optString("name")); + return; + } + store.addObject(entry); + }); + return true; + } catch (Exception e) { + Log.e(TAG, "Failed to load network configs", e); + store.clear(); + return false; + } + } + @Nullable public String createNetwork(@NonNull NetworkConfig config) { var netId = config.getId(); @@ -49,6 +71,7 @@ public String createNetwork(@NonNull NetworkConfig config) { Log.w(TAG, fmt("Network %s already exists", netIdStr)); return null; } + NetworkConfigValidator.validate(config); var inst = getNetworkInstance(config, netIdStr); add(inst); Log.i(TAG, fmt("Created network: %s [%s] bridge=%s", @@ -73,6 +96,7 @@ public String modifyNetwork(@NonNull NetworkConfig config) { Log.w(TAG, fmt("Network %s is not stopped, cannot modify", netIdStr)); return null; } + NetworkConfigValidator.validate(config); removeById(netId); var inst = getNetworkInstance(config, netIdStr); add(inst); @@ -129,7 +153,7 @@ public void stopAll() { toStop.add(inst); }); for (var inst : toStop) - inst.stop(); + inst.stop(true); clear(); } diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/BackendBase.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/BackendBase.java index 9d3fd00b2..1786d6c42 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/BackendBase.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/BackendBase.java @@ -46,7 +46,5 @@ public abstract class BackendBase { public abstract JSONArray listNeighbors(String bridge); - public abstract JSONArray listDhcpLeases(String bridge); - public abstract void populateRuleRoute(@NonNull NetworkInstance inst); } diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/BridgeBackend.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/BridgeBackend.java new file mode 100644 index 000000000..e203b54d8 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/BridgeBackend.java @@ -0,0 +1,147 @@ +package cn.classfun.droidvm.daemon.network.backend; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.List; +import java.util.Map; + +import cn.classfun.droidvm.daemon.network.NetworkInstance; +import cn.classfun.droidvm.daemon.vm.VMInstanceStore; +import cn.classfun.droidvm.lib.network.IPv6Network; +import cn.classfun.droidvm.lib.store.vm.VMNicConfig; + +/** + * Per-network runtime backing one bridge type: owns the data path (Linux + * bridge or gvswitch), per-VLAN L3 services, NIC attach/detach including + * static leases and port forwards, plus the live queries the info UI uses. + */ +public abstract class BridgeBackend { + protected final NetworkInstance inst; + + protected BridgeBackend(@NonNull NetworkInstance inst) { + this.inst = inst; + } + + /** Brings the whole network up; throws with a user-readable reason. */ + public abstract void start() throws Exception; + + /** Tears everything down; must be safe to call after a failed start. */ + public abstract void stop(); + + /** + * Creates the tap device for one VM NIC and applies the per-port + * settings (VLAN, isolation, MAC security), static DHCP leases and + * port forwards. + */ + public abstract void attachNic(@NonNull VMNicConfig nic, @NonNull String tapName) + throws Exception; + + public abstract void detachNic(@NonNull VMNicConfig nic, @NonNull String tapName); + + public abstract JSONArray listAddresses(); + + public abstract JSONArray listInterfaces(VMInstanceStore vms); + + public abstract JSONArray listNeighbors(); + + public abstract JSONArray listDhcpLeases(); + + /** + * Address rows for network_info, one per address with three dimensions: + * {@code {addr, family, vlan, bound, source}} where {@code source} is + * {@code "configured"} (VMM-managed) or {@code "auto"} (link-local, RA, + * DHCP-PD, ...). Returns {@code null} when the backend does not support + * the richer model, in which case the UI falls back to its config + live + * rendering. Default: unsupported. + */ + @Nullable + public JSONArray listAddressEntries() { + return null; + } + + /** Per-VLAN DHCPv6-PD client state for network_info; empty when none. */ + @NonNull + public JSONArray listPdStatus() { + return new JSONArray(); + } + + /** Builds one {@code {addr,family,vlan,bound,source}} row for {@link #listAddressEntries()}. */ + @NonNull + protected static JSONObject addressEntry( + @NonNull String cidr, int vlan, boolean bound, @NonNull String source + ) { + var o = new JSONObject(); + try { + o.put("addr", cidr); + o.put("family", cidr.contains(":") ? 6 : 4); + o.put("vlan", vlan); + o.put("bound", bound); + o.put("source", source); + } catch (JSONException ignored) { + } + return o; + } + + /** Forces a PD renew on one VLAN's client; false when unsupported. */ + public boolean renewPd(int vlanId) { + return false; + } + + /** Releases and re-solicits one VLAN's PD delegation; false when unsupported. */ + public boolean releasePd(int vlanId) { + return false; + } + + /** + * Helper tools this network runs (gvswitch / pbridge / bridgedhcp), as + * {@code [{key, running}]}, so the info UI can list them and open each + * one's captured log. Default: none. + */ + @NonNull + public JSONArray listTools() { + return new JSONArray(); + } + + /** Captured stdout/stderr lines of a named tool, or null if unknown. */ + @Nullable + public List toolLog(@NonNull String key) { + return null; + } + + /** Builds one {@code {key, running}} tool descriptor for {@link #listTools()}. */ + @NonNull + protected static JSONObject toolEntry(@NonNull String key, boolean running) { + var o = new JSONObject(); + try { + o.put("key", key); + o.put("running", running); + } catch (JSONException ignored) { + } + return o; + } + + /** Backend health/status fields for network_info. */ + public abstract void appendInfo(@NonNull JSONObject obj) throws JSONException; + + /** Live per-VLAN IPv6 networks (DHCP-PD delegations); gateway-addressed. */ + @NonNull + public Map getLiveV6Networks() { + return Map.of(); + } + + /** Emits per-tap port-forward install failures into network_info. */ + protected static void appendForwardFailures( + @NonNull JSONObject obj, @NonNull Map> failures + ) throws JSONException { + if (failures.isEmpty()) return; + var ff = new JSONObject(); + for (var e : failures.entrySet()) + ff.put(e.getKey(), new JSONArray(e.getValue())); + obj.put("forward_failures", ff); + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/BridgeDhcp.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/BridgeDhcp.java new file mode 100644 index 000000000..44d455611 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/BridgeDhcp.java @@ -0,0 +1,490 @@ +package cn.classfun.droidvm.daemon.network.backend; + +import static cn.classfun.droidvm.lib.Constants.DATA_DIR; +import static cn.classfun.droidvm.lib.utils.AssetUtils.getAssetBinaryPath; +import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; +import static cn.classfun.droidvm.lib.utils.StringUtils.pathJoin; + +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.File; +import java.io.FileWriter; +import java.security.SecureRandom; +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.zip.CRC32; + +import cn.classfun.droidvm.daemon.network.NetworkInstance; +import cn.classfun.droidvm.daemon.network.backend.gvisor.GvswitchClient; +import cn.classfun.droidvm.daemon.network.backend.pd.Duid; +import cn.classfun.droidvm.lib.network.IPv6Network; +import cn.classfun.droidvm.lib.store.network.Ipv6Source; +import cn.classfun.droidvm.lib.store.network.VlanConfig; +import cn.classfun.droidvm.lib.utils.NetUtils; + +/** + * DHCPv4/DHCPv6/RA (and DHCPv6-PD) service for one Linux-bridge network, + * backed by the bridgedhcp helper: one process per network serving every + * per-VLAN bridge. Pools and static leases are offset-based, so a PD + * renumbering needs no config rewrite; static leases are pushed through + * the unix-socket API instead of dnsmasq's hostsfile/SIGHUP dance, and PD + * prefix changes flow back as JSON event lines on the child's stdout. + */ +public final class BridgeDhcp { + private static final String TAG = "BridgeDhcp"; + private static final String[] DEFAULT_DNS4 = {"8.8.8.8", "1.1.1.1"}; + private static final String[] DEFAULT_DNS6 = {"2001:4860:4860::8888"}; + private static final SecureRandom random = new SecureRandom(); + + /** PD delegation events; address is null when the prefix was lost. */ + public interface PdListener { + void onPdAddressChanged(int vlanId, @Nullable IPv6Network address); + } + + private final NetworkInstance inst; + private final String br; + private final ManagedProcess process; + private final PdListener pdListener; + private final String apiKey; + private GvswitchClient client = null; + + public BridgeDhcp(@NonNull NetworkInstance inst, @Nullable PdListener pdListener) { + this.inst = inst; + this.br = inst.item.optString("bridge_name", ""); + this.pdListener = pdListener; + this.process = new ManagedProcess("bridgedhcp", br); + var key = new byte[24]; + random.nextBytes(key); + var sb = new StringBuilder(); + for (var b : key) sb.append(fmt("%02x", b)); + this.apiKey = sb.toString(); + } + + /** True when any VLAN needs DHCPv4, DHCPv6, RA or a PD client. */ + public static boolean isNeeded(@NonNull List vlans) { + for (var vlan : vlans) { + if (vlan.isDhcp4Enabled() || vlan.isDhcp6Enabled() || vlan.isSlaacEnabled()) + return true; + if (vlan.getIpv6Source() == Ipv6Source.DHCP_PD && vlan.getPdUplink() != null) + return true; + } + return false; + } + + public boolean start() { + try { + writeKeyFile(); + writeConf(); + } catch (Exception e) { + Log.e(TAG, fmt("Failed to write bridgedhcp config for %s", br), e); + return false; + } + client = new GvswitchClient(getSocketFile(br), apiKey); + process.setLineListener(this::onEventLine); + var args = new ArrayList(); + args.add(getAssetBinaryPath("bridgedhcp")); + args.add("--config"); + args.add(getConfFile(br)); + return process.start(args); + } + + public void stop() { + process.stop(); + client = null; + } + + /** + * Rewrites the config (re-resolving PD uplinks) and restarts the helper. + * Used when a PD uplink that was unavailable at start later appears, so + * the now-resolvable interface gets a PD client. Persisted leases and PD + * delegations survive via the state file. + */ + public boolean restart() { + stop(); + return start(); + } + + public boolean isRunning() { + return process.isRunning(); + } + + public int getExitCode() { + return process.getExitCode(); + } + + @NonNull + public List getLog() { + return process.getLog(); + } + + /** Child stdout: single-line JSON events (logs go to stderr). */ + private void onEventLine(@NonNull String line) { + if (!line.startsWith("{")) return; + try { + var ev = new JSONObject(line); + var type = ev.optString("event", ""); + if (!type.equals("pd_prefix") && !type.equals("pd_lost")) return; + if (pdListener == null) return; + int vlanId = Integer.parseInt(ev.optString("tag", "-1")); + if (vlanId < 0) return; + IPv6Network address = null; + if (type.equals("pd_prefix")) + address = IPv6Network.parse(ev.getString("address")); + pdListener.onPdAddressChanged(vlanId, address); + } catch (Exception e) { + Log.w(TAG, fmt("Bad bridgedhcp event: %s", line), e); + } + } + + /** Pushes the current static lease set (both families) for every VLAN. */ + public void reloadStaticLeases() { + var c = client; + if (c == null) return; + for (var vlan : inst.getVlans()) { + var dev = LinuxNetwork.vlanDevice(br, vlan.getVlanId()); + try { + var statics = buildStatics(vlan); + c.put(fmt("/v1/ifaces/%s/statics/4", dev), + new JSONObject().put("statics", statics[0])); + c.put(fmt("/v1/ifaces/%s/statics/6", dev), + new JSONObject().put("statics", statics[1])); + } catch (Exception e) { + Log.w(TAG, fmt("Failed to push static leases for %s", dev), e); + } + } + } + + /** Active leases of every VLAN in the legacy dnsmasq-derived shape. */ + @NonNull + public JSONArray getLeases() { + var out = new JSONArray(); + var c = client; + if (c == null) return out; + for (var vlan : inst.getVlans()) { + var dev = LinuxNetwork.vlanDevice(br, vlan.getVlanId()); + for (var family : new int[]{4, 6}) { + try { + var resp = c.get(fmt("/v1/ifaces/%s/leases/%d", dev, family)); + if (!resp.isSuccess()) continue; + var leases = resp.json().optJSONArray("leases"); + if (leases == null) continue; + for (int i = 0; i < leases.length(); i++) { + var l = leases.optJSONObject(i); + if (l == null) continue; + var obj = new JSONObject(); + obj.put("expires", parseExpiry(l.optString("expiry", ""))); + obj.put("mac", l.optString("mac", "")); + obj.put("ip", l.optString("ip", "")); + obj.put("hostname", l.optString("hostname", "")); + out.put(obj); + } + } catch (Exception e) { + Log.w(TAG, fmt("Failed to list leases for %s/%d", dev, family), e); + } + } + } + return out; + } + + private static long parseExpiry(@NonNull String rfc3339) { + try { + return OffsetDateTime.parse(rfc3339).toEpochSecond(); + } catch (Exception e) { + return 0; + } + } + + /** Full status document of the helper (for network info). */ + @Nullable + public JSONObject getStatus() { + var c = client; + if (c == null) return null; + try { + var resp = c.get("/v1/status"); + if (resp.isSuccess()) return resp.json(); + } catch (Exception e) { + Log.w(TAG, "Failed to query bridgedhcp status", e); + } + return null; + } + + /** + * Per-VLAN DHCPv6-PD client state: {@code [{vlan, state, prefix}]} for + * every served interface that runs a PD client (empty prefix while still + * searching). Derived from the helper's {@code /v1/status} document. + */ + @NonNull + public JSONArray getPdStatus() { + var out = new JSONArray(); + var status = getStatus(); + if (status == null) return out; + var ifaces = status.optJSONArray("ifaces"); + if (ifaces == null) return out; + for (int i = 0; i < ifaces.length(); i++) { + var iface = ifaces.optJSONObject(i); + if (iface == null) continue; + var state = iface.optString("pd_state", ""); + if (state.isEmpty()) continue; // no PD client on this interface + try { + out.put(new JSONObject() + .put("vlan", Integer.parseInt(iface.optString("tag", "-1"))) + .put("state", state) + // the raw delegation P/L (context) ... + .put("prefix", iface.optString("pd_prefix", "")) + // ... and the composed host address P::1/L actually bound + // on the bridge — what the UI shows and what the address + // union deduplicates against. + .put("address", iface.optString("pd_address", ""))); + } catch (Exception e) { + Log.w(TAG, "Bad PD status entry", e); + } + } + return out; + } + + /** Forces a PD renew (RENEW/REBIND, same prefix) on one VLAN's client. */ + public boolean renewPd(int vlanId) { + return pdAction(vlanId, "renew"); + } + + /** Releases and re-solicits the PD delegation on one VLAN's client. */ + public boolean releasePd(int vlanId) { + return pdAction(vlanId, "release"); + } + + private boolean pdAction(int vlanId, @NonNull String action) { + var c = client; + if (c == null) return false; + var dev = LinuxNetwork.vlanDevice(br, vlanId); + try { + return c.post(fmt("/v1/ifaces/%s/pd/%s", dev, action), null).isSuccess(); + } catch (Exception e) { + Log.w(TAG, fmt("PD %s failed for %s", action, dev), e); + return false; + } + } + + private void writeKeyFile() throws Exception { + var file = new File(getKeyFile(br)); + var parent = file.getParentFile(); + if (parent != null && !parent.exists() && !parent.mkdirs()) + throw new RuntimeException(fmt("Failed to create directory %s", parent)); + try (var writer = new FileWriter(file)) { + writer.write(apiKey); + } + if (!file.setReadable(false, false) || !file.setReadable(true, true)) + Log.w(TAG, "Failed to restrict key file permissions"); + } + + private void writeConf() throws Exception { + var cfg = new JSONObject(); + cfg.put("api_socket", getSocketFile(br)); + cfg.put("api_key_file", getKeyFile(br)); + cfg.put("state_file", getStateFile(br)); + var ifaces = new JSONArray(); + for (var vlan : inst.getVlans()) + ifaces.put(buildIface(vlan)); + cfg.put("interfaces", ifaces); + var file = new File(getConfFile(br)); + try (var writer = new FileWriter(file)) { + writer.write(cfg.toString(2)); + } + } + + @NonNull + private JSONObject buildIface(@NonNull VlanConfig vlan) throws Exception { + var obj = new JSONObject(); + obj.put("name", LinuxNetwork.vlanDevice(br, vlan.getVlanId())); + obj.put("tag", String.valueOf(vlan.getVlanId())); + // Served prefixes are authoritative config — bridgedhcp no longer + // derives the pools/SLAAC prefix from the bridge's live addresses. + // v6 is sent only for a static source; DHCP-PD drives it dynamically. + var cidr4 = vlan.getIpv4Cidr(); + if (cidr4 != null) obj.put("prefix4", cidr4); + if (vlan.getIpv6Source() == Ipv6Source.STATIC) { + var cidr6 = vlan.getIpv6Cidr(); + if (cidr6 != null) obj.put("prefix6", cidr6); + } + if (vlan.isDhcp4Enabled()) { + var dns = new JSONArray(); + for (var s : vlan.getDnsServers()) + if (!s.contains(":")) dns.put(s); + if (dns.length() == 0) + for (var s : DEFAULT_DNS4) dns.put(s); + obj.put("dhcp4", new JSONObject() + .put("pool_offset_start", vlan.getDhcp4OffsetStart()) + .put("pool_offset_end", vlan.getDhcp4OffsetEnd()) + .put("lease_time", "12h") + .put("dns", dns)); + } + if (vlan.isDhcp6Enabled()) { + var dns = new JSONArray(); + for (var s : vlan.getDnsServers()) + if (s.contains(":")) dns.put(s); + if (dns.length() == 0) + for (var s : DEFAULT_DNS6) dns.put(s); + obj.put("dhcp6", new JSONObject() + .put("pool_offset_start", vlan.getDhcp6OffsetStart()) + .put("pool_offset_end", vlan.getDhcp6OffsetEnd()) + .put("lease_time", "12h") + .put("dns", dns)); + } + if (vlan.isSlaacEnabled()) obj.put("slaac", true); + if (vlan.getIpv6Source() == Ipv6Source.DHCP_PD && vlan.getPdUplink() != null) { + var uplink = UplinkResolver.resolve(vlan.getPdUplink()); + if (uplink == null) { + Log.w(TAG, fmt( + "PD uplink \"%s\" not found, skipping PD for vlan %d", + vlan.getPdUplink(), vlan.getVlanId() + )); + } else { + var crc = new CRC32(); + crc.update(inst.getId().toString().getBytes()); + crc.update(vlan.getVlanId()); + // CRC32 is an unsigned 32-bit value; write it as-is. Casting + // to int makes it negative when bit 31 is set, and bridgedhcp + // parses "iaid" into a Go uint32, which rejects a negative + // number — the whole helper then fails to start. + // No prefix_len: bridgedhcp binds the host at the delegation's + // own length L (suffix ::1), and gates SLAAC/DHCPv6 on L. + obj.put("pd", new JSONObject() + .put("uplink", uplink) + .put("duid", Duid.format(resolvePdDuid(vlan))) + .put("iaid", crc.getValue()) + .put("suffix", "::1") + .put("route_table", resolvePdRouteTable(vlan))); + } + } + var statics = buildStatics(vlan); + if (statics[0].length() > 0) obj.put("statics4", statics[0]); + if (statics[1].length() > 0) obj.put("statics6", statics[1]); + return obj; + } + + /** + * Builds the [v4, v6] static binding arrays of one VLAN from the VM NIC + * configs (mac -> offset, matching the NIC's DHCP lease settings). + */ + @NonNull + private JSONArray[] buildStatics(@NonNull VlanConfig vlan) throws Exception { + var v4 = new JSONArray(); + var v6 = new JSONArray(); + var netId = inst.getId().toString(); + var vms = inst.getStore().context.getVMs(); + if (vms == null) return new JSONArray[]{v4, v6}; + vms.forEach((vmId, vm) -> { + final int[] idx = {0}; + var failed = new Exception[1]; + vm.forEachNic(nic -> { + int nicIdx = idx[0]++; + try { + if (!netId.equals(nic.getNetworkId())) return; + var mac = nic.getMacAddress(); + if (mac == null) return; + var nicVlan = nic.resolveDhcpVlan(inst); + if (nicVlan == null || nicVlan.getVlanId() != vlan.getVlanId()) return; + var id = fmt("%s-%d", vmId, nicIdx); + if (nic.isDhcp4LeaseEnabled() && vlan.isDhcp4Enabled()) { + v4.put(new JSONObject() + .put("id", id) + .put("mac", mac) + .put("offset", nic.getDhcp4Offset())); + } + if (nic.isDhcp6LeaseEnabled() && vlan.isDhcp6Enabled()) { + v6.put(new JSONObject() + .put("id", id) + .put("mac", mac) + .put("offset", nic.getDhcp6Offset())); + } + } catch (Exception e) { + failed[0] = e; + } + }); + if (failed[0] != null) + Log.w(TAG, fmt("Bad static lease config for VM %s", vmId), failed[0]); + }); + return new JSONArray[]{v4, v6}; + } + + /** First id of the range handed out to bridgedhcp PD route tables. */ + private static final long PD_ROUTE_TABLE_BASE = 9991; + /** Serializes allocations across concurrently starting networks. */ + private static final Object routeTableLock = new Object(); + + /** + * The PD routing table of one VLAN: the persisted allocation when + * present, else the lowest id from PD_ROUTE_TABLE_BASE upward that no + * VLAN of any network (running or stopped) holds, persisted + * immediately. Stability matters: the id is bridgedhcp's crash-cleanup + * key — a bridge that comes back under a different id would leave the + * previous run's rules behind, and reusing a *live* network's id would + * wipe its rules at startup. + */ + private long resolvePdRouteTable(@NonNull VlanConfig vlan) { + synchronized (routeTableLock) { + var existing = vlan.getPdRouteTable(); + if (existing != 0) return existing; + var used = new java.util.HashSet(); + inst.getStore().forEach((id, other) -> { + for (var v : other.getVlans()) + used.add(v.getPdRouteTable()); + }); + var table = PD_ROUTE_TABLE_BASE; + while (used.contains(table)) table++; + vlan.setPdRouteTable(table); + Log.i(TAG, fmt("Allocated PD route table %d for %s vlan %d", + table, br, vlan.getVlanId())); + return table; + } + } + + /** + * The PD DUID: the configured one, else a generated DUID-LL persisted + * back into the VLAN config so the delegation survives restarts. + */ + @NonNull + private byte[] resolvePdDuid(@NonNull VlanConfig vlan) { + var configured = vlan.getPdDuid(); + if (configured != null && !configured.isEmpty()) { + try { + return Duid.parse(configured); + } catch (Exception e) { + Log.w(TAG, fmt("Invalid DUID \"%s\", generating one", configured), e); + } + } + var mac = inst.getBridgeMacAddress(); + if (mac == null) mac = inst.item.optString("generated_mac", ""); + if (mac == null || mac.isEmpty()) mac = NetUtils.generateRandomMac(); + var duid = Duid.fromLinkLayer(mac); + vlan.setPdDuid(Duid.format(duid)); + return duid; + } + + @NonNull + public static String getConfFile(@NonNull String br) { + return pathJoin(DATA_DIR, "run", fmt("bridgedhcp-%s.json", br)); + } + + @NonNull + public static String getKeyFile(@NonNull String br) { + return pathJoin(DATA_DIR, "run", fmt("bridgedhcp-%s.key", br)); + } + + @NonNull + public static String getSocketFile(@NonNull String br) { + return pathJoin(DATA_DIR, "run", fmt("bridgedhcp-%s.sock", br)); + } + + @NonNull + public static String getStateFile(@NonNull String br) { + return pathJoin(DATA_DIR, "run", fmt("bridgedhcp-%s.state.json", br)); + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/DefaultRouterWatcher.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/DefaultRouterWatcher.java index c84cf33c8..4c01ec4bc 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/DefaultRouterWatcher.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/DefaultRouterWatcher.java @@ -1,7 +1,5 @@ package cn.classfun.droidvm.daemon.network.backend; -import static cn.classfun.droidvm.lib.utils.RunUtils.run; -import static cn.classfun.droidvm.lib.utils.RunUtils.runListQuiet; import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; import android.util.Log; @@ -10,100 +8,286 @@ import androidx.annotation.Nullable; import java.util.ArrayList; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import cn.classfun.droidvm.daemon.network.NetworkInstance; import cn.classfun.droidvm.daemon.server.ServerContext; +import cn.classfun.droidvm.lib.Constants; public final class DefaultRouterWatcher { private static final String TAG = "DefaultRouterWatcher"; private static final long POLL_INTERVAL_SEC = 5; + /** + * Interface-name prefixes whose IPv4 addresses count as the phone's own + * reachable IPs for port-forward DNAT scoping: Wi-Fi, cellular, VPN, + * ethernet, and hotspot/USB/BT tethering. Passed to netbox, which also + * drops bridge devices and pbridge-offload addresses. Cellular names other + * than rmnet_data (ccmni, pdp_ip…) are intentionally not matched — same + * assumption the iptables EXT_IFACES list already makes. + */ + private static final List HOST_IFACE_PREFIXES = List.of( + "wlan", "rmnet_data", "tun", "eth", + "ap", "swlan", "softap", "rndis", "usb", "bt-pan" + ); private final ServerContext context; - private final List rules = new ArrayList<>(); - private ScheduledExecutorService scheduler; - private String lastDefault = null; + private final List listeners = new java.util.concurrent.CopyOnWriteArrayList<>(); + private final List hostIpListeners = new java.util.concurrent.CopyOnWriteArrayList<>(); + private volatile ScheduledExecutorService scheduler; + private String lastDefault4 = null; + private String lastDefault6 = null; + /** Phone's own IPv4 addresses; the netbox host-ips policy decides the set. */ + private volatile Set hostIpv4 = Set.of(); + /** netbox monitor-addr: pushes host-IP changes the instant they happen. */ + private ManagedProcess hostIpMonitor = null; + /** Gates monitor (re)spawn so a post-stop tick can't resurrect it. */ + private volatile boolean monitorEnabled = false; public DefaultRouterWatcher(@NonNull ServerContext context) { this.context = context; } + /** + * The routing table of the current default network, per family: Android + * keeps the default route out of main and selects it with the netd rule + * "from all fwmark 0x0/0xffff iif lo lookup {table}". + */ @Nullable - private String getCurrentDefault() { - var result = runListQuiet("ip", "rule", "show"); - if (!result.isSuccess()) { - result.printLog("ip-rule"); - throw new RuntimeException("Failed to list IP rules"); - } - var data = result.getOutString(); - for (var line : data.split("\n")) { - if (!line.contains("fwmark 0x0/0xffff")) continue; - var idx = line.indexOf(" lookup "); - if (idx < 0) continue; - return line.substring(idx + (" lookup ".length())).trim(); + private String currentDefaultTable(boolean ipv6) { + // netd selects the default network with "from all fwmark 0x0/0xffff + // iif lo lookup {table}" (mark 0, mask 0xffff). The kernel omits + // FRA_FWMARK when the mark is 0, so match on the 0xffff mask alone. + var rows = Netbox.ruleList(ipv6); + for (int i = 0; i < rows.length(); i++) { + var r = rows.optJSONObject(i); + if (r == null) continue; + if (r.optInt("fwmark", 0) != 0) continue; + if (r.optInt("fwmask", 0) != 0xffff) continue; + return String.valueOf(r.optInt("table", 0)); } return null; } - private void clearRule() { - for (var rule : rules) - run(fmt("ip rule del %s", rule)); - rules.clear(); + private void ruleDel(boolean ipv6, @NonNull String dev, @NonNull String table) { + Netbox.ruleDel(dev, table, ipv6); } - private void addRule(@NonNull String table, @NonNull String br) { - var rule = fmt("from all iif %s lookup %s", br, table); - run(fmt("ip rule add %s", rule)); - rules.add(rule); + private void ruleAdd(boolean ipv6, @NonNull String dev, @NonNull String table) { + Netbox.ruleAdd(dev, table, ipv6); } - private void setDefaultRouterForNetworks(@NonNull String net) { - context.getNetworks().forEach((uuid, inst) -> - addRule(net, inst.item.optString("bridge_name", ""))); + /** + * Reconciles one family's "iif {bridge} lookup {default-table}" rules + * (the egress half of guest routing: forwarded packets carry no fwmark + * and Android's netd rules end in "from all unreachable", so each + * bridge needs an explicit rule into the default network's table). + * Declarative per tick: deletes stale rules — wrong table, duplicates + * (added twice across restarts) and [detached] leftovers from a + * recreated bridge — and adds what is missing. Stale-rule deletion by + * spec may remove either twin first; the next tick converges. + */ + private void reconcileRules(boolean ipv6, @Nullable String table) { + var desired = new LinkedHashSet(); + var bridges = new ArrayList(); + context.getNetworks().forEach((uuid, inst) -> { + var br = inst.getBridgeName(); + if (br != null && !br.isEmpty()) bridges.add(br); + if (table != null) desired.addAll(inst.getL3Devices()); + }); + + var satisfied = new LinkedHashSet(); + var rows = Netbox.ruleList(ipv6); + for (int i = 0; i < rows.length(); i++) { + var r = rows.optJSONObject(i); + if (r == null) continue; + var dev = r.optString("iif", ""); + // only our "iif lookup " rules: skip rules with no + // iif, and netd's marked rules (any fwmark/mask present) + if (dev.isEmpty()) continue; + if (!r.isNull("fwmark") || !r.isNull("fwmask")) continue; + boolean ours = false; + for (var br : bridges) + if (dev.startsWith(br)) { ours = true; break; } + if (!ours) continue; + var tbl = String.valueOf(r.optInt("table", 0)); + boolean stale = r.optBoolean("detached", false) + || table == null || !tbl.equals(table) + || !desired.contains(dev) || satisfied.contains(dev); + if (stale) { + Log.i(TAG, fmt("Removing stale rule: iif %s lookup %s (v%d)", + dev, tbl, ipv6 ? 6 : 4)); + ruleDel(ipv6, dev, tbl); + } else { + satisfied.add(dev); + } + } + for (var dev : desired) { + if (satisfied.contains(dev)) continue; + Log.i(TAG, fmt("Adding rule: iif %s lookup %s (v%d)", + dev, table, ipv6 ? 6 : 4)); + ruleAdd(ipv6, dev, table); + } } private synchronized void runOnce() { + ensureHostIpMonitor(); + updateHostIps(); + updateDefaultRouter(); + } + + private void updateDefaultRouter() { try { - var newDefault = getCurrentDefault(); - if (newDefault == null) return; - if (lastDefault == null) { - Log.i(TAG, fmt("Found default router changed %s", newDefault)); - } else { - if (newDefault.equals(lastDefault)) return; - Log.i(TAG, fmt("Default router changed from %s to %s", lastDefault, newDefault)); - } - clearRule(); - setDefaultRouterForNetworks(newDefault); - lastDefault = newDefault; + var new4 = currentDefaultTable(false); + var new6 = currentDefaultTable(true); + reconcileRules(false, new4); + reconcileRules(true, new6); + boolean changed = !java.util.Objects.equals(new4, lastDefault4) + || !java.util.Objects.equals(new6, lastDefault6); + if (changed) + Log.i(TAG, fmt("Default network now v4=%s v6=%s", new4, new6)); + lastDefault4 = new4; + lastDefault6 = new6; + // notify only on appearance/change, not on loss: a reconnect to + // the same network re-triggers because the loss nulled the state + if (changed && (new4 != null || new6 != null)) + notifyChange(); } catch (Exception e) { Log.w(TAG, "Failed to update default router", e); stop(); } } + private void notifyChange() { + for (var listener : listeners) { + try { + listener.run(); + } catch (Exception e) { + Log.w(TAG, "Default-network listener failed", e); + } + } + } + + /** + * Backstop poll of the phone's own IPv4 addresses for port-forward DNAT + * scoping. The {@code netbox monitor-addr} stream is the primary, instant + * source ({@link #onMonitorSet}); this tick covers a missed event or a dead + * monitor. On a transient query failure the previous set is kept (returning + * empty would flap every forward), so this never throws out of the tick. + */ + private void updateHostIps() { + try { + applyHostIps(Netbox.hostIpv4(HOST_IFACE_PREFIXES, Constants.PBRIDGE_OFFLOAD_MAGIC)); + } catch (Exception e) { + Log.w(TAG, "Failed to poll host IPv4 addresses", e); + } + } + + /** Diffs the fresh set against the cache and fires listeners if it moved. */ + private void applyHostIps(@NonNull Set fresh) { + if (fresh.equals(hostIpv4)) return; + Log.i(TAG, fmt("Host IPv4 set changed %s -> %s", hostIpv4, fresh)); + hostIpv4 = Set.copyOf(fresh); + for (var listener : hostIpListeners) { + try { + listener.run(); + } catch (Exception e) { + Log.w(TAG, "Host-IP listener failed", e); + } + } + } + + /** + * Receives a new host-IP set from the netbox monitor (on its process + * thread). Hops onto the scheduler so all host-IP state mutates on one + * thread, shared with the backstop tick; dropped if we're stopping. + */ + private void onMonitorSet(@NonNull Set fresh) { + var s = scheduler; + if (s == null) return; + try { + s.execute(() -> applyHostIps(fresh)); + } catch (java.util.concurrent.RejectedExecutionException ignored) { + // scheduler shutting down; the next start() reseeds + } + } + + /** Starts the monitor, or restarts it if it died (called each tick). */ + private void ensureHostIpMonitor() { + if (!monitorEnabled) return; + if (hostIpMonitor != null && hostIpMonitor.isRunning()) return; + if (hostIpMonitor != null) + Log.w(TAG, "host-IP monitor exited; restarting"); + hostIpMonitor = Netbox.startHostIpMonitor( + HOST_IFACE_PREFIXES, Constants.PBRIDGE_OFFLOAD_MAGIC, this::onMonitorSet); + } + + /** Current snapshot of the phone's own IPv4 addresses (immutable). */ + @NonNull + public Set getHostIpv4Addresses() { + return hostIpv4; + } + + /** Notified (on the watcher thread) when the host IPv4 set changes. */ + public void addHostIpListener(@NonNull Runnable listener) { + hostIpListeners.add(listener); + } + + public void removeHostIpListener(@NonNull Runnable listener) { + hostIpListeners.remove(listener); + } + + /** Notified (on the watcher thread) when the default network changes. */ + public void addListener(@NonNull Runnable listener) { + listeners.add(listener); + } + + public void removeListener(@NonNull Runnable listener) { + listeners.remove(listener); + } + + /** Installs the new network's rules immediately (the tick would lag 5s). */ public synchronized void setForNewNetwork(@NonNull NetworkInstance inst) { - if (lastDefault == null) return; - addRule(lastDefault, inst.item.optString("bridge_name", "")); + try { + reconcileRules(false, currentDefaultTable(false)); + reconcileRules(true, currentDefaultTable(true)); + } catch (Exception e) { + Log.w(TAG, "Failed to install rules for new network", e); + } } public void start() { + monitorEnabled = true; scheduler = Executors.newSingleThreadScheduledExecutor(r -> { var t = new Thread(r, TAG); t.setDaemon(true); return t; }); + // the first tick (delay 0) spawns the monitor via ensureHostIpMonitor scheduler.scheduleWithFixedDelay( this::runOnce, 0, POLL_INTERVAL_SEC, TimeUnit.SECONDS ); } - public void stop() { + public synchronized void stop() { + monitorEnabled = false; + if (hostIpMonitor != null) { + hostIpMonitor.stop(); + hostIpMonitor = null; + } if (scheduler != null) { scheduler.shutdownNow(); scheduler = null; } - clearRule(); + // sweep every rule of ours, both families + try { + reconcileRules(false, null); + reconcileRules(true, null); + } catch (Exception e) { + Log.w(TAG, "Failed to sweep rules on stop", e); + } } } diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/Dnsmasq.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/Dnsmasq.java deleted file mode 100644 index 0c41b7127..000000000 --- a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/Dnsmasq.java +++ /dev/null @@ -1,217 +0,0 @@ -package cn.classfun.droidvm.daemon.network.backend; - -import static cn.classfun.droidvm.lib.Constants.DATA_DIR; -import static cn.classfun.droidvm.lib.utils.AssetUtils.getPrebuiltBinaryPath; -import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; -import static cn.classfun.droidvm.lib.utils.StringUtils.pathJoin; - -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.io.BufferedReader; -import java.io.File; -import java.io.InputStreamReader; -import java.io.InterruptedIOException; -import java.util.ArrayList; -import java.util.concurrent.TimeUnit; - -import cn.classfun.droidvm.daemon.network.NetworkInstance; -import cn.classfun.droidvm.lib.network.IPv4Address; -import cn.classfun.droidvm.lib.network.IPv4Network; -import cn.classfun.droidvm.lib.store.network.NetworkState; -import cn.classfun.droidvm.lib.utils.ProcessUtils; - -public final class Dnsmasq { - private static final String TAG = "Dnsmasq"; - private final NetworkInstance inst; - private Process dnsmasqProcess = null; - private Thread dnsmasqMonitor = null; - private volatile boolean dnsmasqRunning = false; - private int dnsmasqExitCode = -1; - - public Dnsmasq(NetworkInstance inst) { - this.inst = inst; - } - - @NonNull - private static String resolveDnsmasqBinary() { - var prebuilt = getPrebuiltBinaryPath("dnsmasq"); - var file = new File(prebuilt); - if (file.isFile() && file.canExecute()) - return prebuilt; - return "dnsmasq"; - } - - private void startDnsmasqProcess( - ) { - var bridge = inst.item.optString("bridge_name", ""); - var rangeStart = inst.item.optString("dhcp_range_start", ""); - var rangeEnd = inst.item.optString("dhcp_range_end", ""); - var router = findRouterAddress(rangeStart); - var dnsmasq = resolveDnsmasqBinary(); - Log.i(TAG, fmt("Starting dnsmasq on %s (%s - %s) using %s", - bridge, rangeStart, rangeEnd, dnsmasq)); - var pidFile = getDnsmasqPidFile(); - var leaseFile = getDnsmasqLeaseFile(); - try { - var args = new ArrayList(); - args.add(dnsmasq); - args.add(fmt("--interface=%s", bridge)); - args.add("--bind-interfaces"); - args.add(fmt("--dhcp-range=%s,%s,12h", rangeStart, rangeEnd)); - if (router != null) - args.add(fmt("--dhcp-option=option:router,%s", router)); - var dnsServers = new ArrayList(); - for (var iter : inst.item.get("dns_servers")) { - var s = iter.getValue().asString(); - if (s != null && !s.isEmpty()) dnsServers.add(s); - } - if (!dnsServers.isEmpty()) - args.add(fmt("--dhcp-option=option:dns-server,%s", String.join(",", dnsServers))); - args.add("--port=0"); - args.add("--no-resolv"); - args.add("--no-daemon"); - args.add("--keep-in-foreground"); - args.add(fmt("--pid-file=%s", pidFile)); - args.add(fmt("--dhcp-leasefile=%s", leaseFile)); - args.add("--log-queries"); - args.add("--log-dhcp"); - var pb = new ProcessBuilder(args); - pb.redirectErrorStream(true); - dnsmasqExitCode = 0; - dnsmasqProcess = pb.start(); - Log.i(TAG, fmt("dnsmasq started on %s", bridge)); - } catch (Exception e) { - Log.e(TAG, fmt("Failed to start dnsmasq on %s", bridge), e); - } - } - - @Nullable - private String findRouterAddress(@NonNull String rangeStart) { - IPv4Address dhcpStart = null; - try { - if (!rangeStart.isEmpty()) dhcpStart = IPv4Address.parse(rangeStart); - } catch (Exception ignored) { - } - - IPv4Network fallback = null; - for (var iter : inst.item.get("ipv4_addresses")) { - try { - var cidr = IPv4Network.parse(iter.getValue().asString()); - if (fallback == null) fallback = cidr; - if (dhcpStart != null && cidr.contains(dhcpStart)) - return cidr.address().toString(); - } catch (Exception ignored) { - } - } - return fallback != null ? fallback.address().toString() : null; - } - - private void stopDnsmasqProcess(@Nullable Process process) { - var bridge = inst.item.optString("bridge_name", ""); - Log.i(TAG, fmt("Stopping dnsmasq on %s", bridge)); - if (process != null) { - process.destroy(); - try { - process.waitFor(3, TimeUnit.SECONDS); - } catch (InterruptedException ignored) { - Thread.currentThread().interrupt(); - } - process.destroyForcibly(); - try { - process.waitFor(); - } catch (InterruptedException ignored) { - Thread.currentThread().interrupt(); - } - } - var pidFile = getDnsmasqPidFile(); - if (new File(pidFile).exists()) - ProcessUtils.shellKillProcessFile(pidFile); - } - - public void startDnsmasq() { - stopDnsmasq(); - var br = inst.item.optString("bridge_name", ""); - startDnsmasqProcess(); - if (dnsmasqProcess == null) { - Log.e(TAG, fmt("dnsmasq failed to start on %s", br)); - dnsmasqRunning = false; - return; - } - dnsmasqRunning = true; - Log.i(TAG, fmt("dnsmasq running on %s", br)); - dnsmasqMonitor = new Thread(this::dnsmasqMonitorThread, fmt("dnsmasq-mon-%s", br)); - dnsmasqMonitor.setDaemon(true); - dnsmasqMonitor.start(); - } - - private void dnsmasqMonitorThread() { - var br = inst.item.optString("bridge_name", ""); - try { - try (var reader = new BufferedReader( - new InputStreamReader(dnsmasqProcess.getInputStream()))) { - String line; - while ((line = reader.readLine()) != null) - Log.d(fmt("dnsmasq-%s", br), line); - } - int code = dnsmasqProcess.waitFor(); - dnsmasqExitCode = code; - dnsmasqRunning = false; - if (inst.getState() == NetworkState.RUNNING) { - Log.w(TAG, fmt("dnsmasq on %s exited unexpectedly (code %d)", br, code)); - } else { - Log.i(TAG, fmt("dnsmasq on %s exited (code %d)", br, code)); - } - } catch (InterruptedException | InterruptedIOException e) { - Thread.currentThread().interrupt(); - Log.d(TAG, fmt("dnsmasq monitor on %s interrupted", br)); - } catch (Exception e) { - Log.e(TAG, fmt("dnsmasq monitor on %s failed", br), e); - } finally { - dnsmasqRunning = false; - } - } - - public void stopDnsmasq() { - if (!dnsmasqRunning) return; - if (dnsmasqMonitor != null) { - dnsmasqMonitor.interrupt(); - dnsmasqMonitor = null; - } - if (dnsmasqProcess != null || inst.item.optBoolean("dhcp_enabled", false)) { - stopDnsmasqProcess(dnsmasqProcess); - dnsmasqProcess = null; - } - dnsmasqRunning = false; - } - - @NonNull - public String getDnsmasqLeaseFile() { - return getDnsmasqLeaseFile(inst.item.optString("bridge_name", "")); - } - - @NonNull - public String getDnsmasqPidFile() { - return getDnsmasqPidFile(inst.item.optString("bridge_name", "")); - } - - @NonNull - public static String getDnsmasqLeaseFile(@NonNull String br) { - return pathJoin(DATA_DIR, "run", fmt("dnsmasq-%s.leases", br)); - } - - @NonNull - public static String getDnsmasqPidFile(@NonNull String br) { - return pathJoin(DATA_DIR, "run", fmt("dnsmasq-%s.pid", br)); - } - - public int getDnsmasqExitCode() { - return dnsmasqExitCode; - } - - public boolean isDnsmasqRunning() { - return dnsmasqRunning; - } -} diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/FirewallHelper.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/FirewallHelper.java index dd2cbad82..b46e54182 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/FirewallHelper.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/FirewallHelper.java @@ -4,6 +4,7 @@ import androidx.annotation.Nullable; import cn.classfun.droidvm.daemon.network.NetworkInstance; +import cn.classfun.droidvm.lib.network.IPv6Network; @SuppressWarnings({"BooleanMethodIsAlwaysInverted", "unused", "UnusedReturnValue"}) public abstract class FirewallHelper { @@ -16,11 +17,40 @@ public abstract class FirewallHelper { public abstract void deinitNetwork(NetworkInstance inst); - public abstract boolean applyForward( + /** Allow traffic for a delegated prefix acquired while the network runs. */ + public abstract void addLiveV6Subnet( + @NonNull NetworkInstance inst, @NonNull String dev, @NonNull IPv6Network net); + + public abstract void removeLiveV6Subnet( + @NonNull NetworkInstance inst, @NonNull String dev, @NonNull IPv6Network net); + + /** + * Installs the host-IP-independent half of a port forward: the FORWARD + * ACCEPT for the guest target and, when hairpinSubnet is set, the + * NAT-loopback MASQUERADE so a guest in that subnet reaching this forward + * via the host has its source masqueraded (reply returns via the gateway). + * The actual DNAT is installed per host IP via {@link #applyDnat}. + */ + public abstract boolean applyForwardBase( @NonNull String bridge, @NonNull String guestIp, @NonNull String protocol, - @Nullable String hostIp, int hostPort, int guestPort); + int guestStart, int guestEnd, @Nullable String hairpinSubnet); - public abstract boolean removeForward( + public abstract boolean removeForwardBase( @NonNull String bridge, @NonNull String guestIp, @NonNull String protocol, - @Nullable String hostIp, int hostPort, int guestPort); + int guestStart, int guestEnd, @Nullable String hairpinSubnet); + + /** + * Installs one DNAT rule scoped to a specific host IP ({@code -d hostIp}), + * so only traffic actually destined to the phone is redirected — transit + * traffic (other networks, tethered clients) is left alone. Called once + * per current host IP; rules are added/removed incrementally as the host + * IP set changes. + */ + public abstract boolean applyDnat( + @NonNull String bridge, @NonNull String guestIp, @NonNull String protocol, + @NonNull String hostIp, int hostStart, int hostEnd, int guestStart, int guestEnd); + + public abstract boolean removeDnat( + @NonNull String bridge, @NonNull String guestIp, @NonNull String protocol, + @NonNull String hostIp, int hostStart, int hostEnd, int guestStart, int guestEnd); } diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/LinuxBridgeBackend.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/LinuxBridgeBackend.java new file mode 100644 index 000000000..8a38f787e --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/LinuxBridgeBackend.java @@ -0,0 +1,751 @@ +package cn.classfun.droidvm.daemon.network.backend; + +import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; + +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import cn.classfun.droidvm.daemon.network.NetworkInstance; +import cn.classfun.droidvm.daemon.vm.VMInstanceStore; +import cn.classfun.droidvm.lib.Constants; +import cn.classfun.droidvm.lib.network.IPNetwork; +import cn.classfun.droidvm.lib.network.IPv4Network; +import cn.classfun.droidvm.lib.network.IPv6Network; +import cn.classfun.droidvm.lib.store.network.Ipv6Source; +import cn.classfun.droidvm.lib.store.network.UplinkMode; +import cn.classfun.droidvm.lib.store.network.VlanConfig; +import cn.classfun.droidvm.lib.store.vm.VMNicConfig; +import cn.classfun.droidvm.lib.utils.NetUtils; + +/** + * Linux-bridge data path: kernel bridge (+ VLAN filtering and per-VLAN + * 802.1q subinterfaces for L3 mode), bridgedhcp for DHCP/RA/DHCPv6-PD, + * iptables for SNAT and port forwards, pbridge for Wi-Fi L2 uplinks. + */ +public final class LinuxBridgeBackend extends BridgeBackend { + private static final String TAG = "LinuxBridgeBackend"; + private final LinuxNetwork net; + private final FirewallHelper firewall; + private final String br; + private BridgeDhcp dhcp = null; + private Pbridge pbridge = null; + private String resolvedUplink = null; + private boolean uplinkEnslaved = false; + /** vlanId -> gateway-addressed /64 from a live PD delegation. */ + private final Map liveV6 = new ConcurrentHashMap<>(); + private final Map macSecurityActive = new ConcurrentHashMap<>(); + /** tapName -> forwards this NIC actually installed (for exact teardown). */ + private final Map> installedForwards = new ConcurrentHashMap<>(); + /** tapName -> human-readable forward install failures. */ + private final Map> forwardFailures = new ConcurrentHashMap<>(); + /** vids whose per-VLAN bridge + trunk leg exist (configured or on-demand). */ + private final java.util.Set createdVlanBridges = ConcurrentHashMap.newKeySet(); + /** DHCP-PD vlanId -> uplink device written into the running bridgedhcp + * config ("" when it was unresolved). Lets us reload when it appears. */ + private final Map pdConfiguredUplink = new ConcurrentHashMap<>(); + /** Host IPv4 set the installed DNAT rules currently reflect; kept in sync. */ + private final java.util.Set appliedHostIps = new java.util.LinkedHashSet<>(); + /** Re-scopes DNAT rules when the phone's host IPv4 set changes. */ + private Runnable hostIpListener = null; + + /** Records one installed forward so detach removes exactly what was added. */ + private static final class InstalledForward { + final String guestIp, proto, hairpin; + final int hs, he, gs, ge; + InstalledForward(String guestIp, String proto, int hs, int he, int gs, int ge, String hairpin) { + this.guestIp = guestIp; this.proto = proto; this.hairpin = hairpin; + this.hs = hs; this.he = he; this.gs = gs; this.ge = ge; + } + } + + public LinuxBridgeBackend(@NonNull NetworkInstance inst) { + super(inst); + this.net = inst.getStore().backend; + this.firewall = inst.getStore().firewall; + this.br = inst.item.optString("bridge_name", ""); + } + + @Override + public void start() throws Exception { + if (net.isInterfaceExists(br)) { + Log.w(TAG, fmt("Interface %s already exists, deleting", br)); + net.deleteBridge(br); + } + if (!net.createBridge(br)) + throw new RuntimeException(fmt("Failed to create bridge %s", br)); + var mac = resolveBridgeMac(); + if (mac != null && !net.setMacAddress(br, mac)) + Log.w(TAG, fmt("Failed to set MAC %s on %s", mac, br)); + net.setStp(br, inst.isStp()); + switch (inst.getUplinkMode()) { + case L2: + startL2(); + break; + case L3: + startL3(); + break; + } + if (!net.setLinkState(br, true)) + throw new RuntimeException(fmt("Failed to bring up bridge %s", br)); + if (inst.getUplinkMode() == UplinkMode.L3) + startL3Services(); + } + + /** + * The bridge needs a stable MAC: the configured one for L3/none, an + * auto-generated persisted one otherwise. Without it the bridge MAC + * would follow whichever port is enslaved. + */ + @Nullable + private String resolveBridgeMac() { + var mac = inst.getBridgeMacAddress(); + if (mac != null) return mac; + var generated = inst.item.optString("generated_mac", ""); + if (generated == null || generated.isEmpty()) { + generated = NetUtils.generateRandomMac(); + inst.item.set("generated_mac", generated); + } + return generated; + } + + private void startL2() throws Exception { + var configured = inst.getL2Uplink(); + if (configured == null) + throw new RuntimeException("L2 network has no uplink configured"); + var uplink = UplinkResolver.resolve(configured); + if (uplink == null) + throw new RuntimeException(fmt("Uplink \"%s\" not found", configured)); + resolvedUplink = uplink; + // honour the configured toggle, but force pseudo-bridging when the + // resolved device can't be enslaved (Wi-Fi STA / non-ethernet) + boolean pseudo = inst.isL2PseudoBridge() + || UplinkResolver.assumeDontBridge(uplink); + if (pseudo) { + pbridge = new Pbridge(uplink, br); + if (!pbridge.start()) + throw new RuntimeException(fmt( + "Failed to start pseudo-bridge on %s", uplink)); + } else { + uplinkEnslaved = net.addInterface(br, uplink); + if (!uplinkEnslaved) throw new RuntimeException(fmt( + "Uplink %s cannot be bridged (IFF_DONT_BRIDGE?); enable pseudo-bridge", + uplink + )); + } + } + + private void startL3() { + // Stock GKI has no bridge VLAN filtering: each tagged VLAN gets its + // own bridge ({br}v{hex}) plus an 802.1q trunk leg ({br}.{hex}) on + // the main bridge. Access ports attach to the per-VLAN bridge, + // trunk ports to the main bridge; L3 lives on the per-VLAN bridge. + // VLAN 0 is the untagged domain: its L3 lives directly on the main + // bridge (no per-VLAN bridge, no VID-0 leg — that would only catch + // priority-tagged frames, not untagged traffic). + // + // These bridges and legs face the VMs, so harden them as a router's + // LAN side: disable IPv6 RA acceptance so a VM's RAs can't + // autoconfigure an address or default route on the host (the kernel + // default accept_ra=1 would otherwise allow it). + net.disableAcceptRa(br); // main bridge (VLAN 0 / trunk backbone) + for (var vlan : inst.getVlans()) { + int vid = vlan.getVlanId(); + var dev = LinuxNetwork.vlanDevice(br, vid); + if (vid != 0) { + if (!net.createVlanBridge(br, vid)) + Log.w(TAG, fmt("Failed to create per-VLAN bridge for vid %d", vid)); + createdVlanBridges.add(vid); + net.disableAcceptRa(LinuxNetwork.vlanTrunkLeg(br, vid)); // vlan if + net.disableAcceptRa(dev); // per-VLAN bridge + } + var net4 = vlan.getIpv4Network(); + if (net4 != null && !net.addAddress(dev, net4)) + Log.w(TAG, fmt("Failed to add IPv4 %s to %s", net4, dev)); + for (var cidr : vlan.getIpv4Secondary()) + addAddressSafe(dev, cidr, false); + var net6 = vlan.getIpv6Network(); + if (net6 != null && !net.addAddress(dev, net6)) + Log.w(TAG, fmt("Failed to add IPv6 %s to %s", net6, dev)); + for (var cidr : vlan.getIpv6Secondary()) + addAddressSafe(dev, cidr, true); + } + } + + private void addAddressSafe(@NonNull String dev, @NonNull String cidr, boolean v6) { + try { + if (v6) net.addAddress(dev, IPv6Network.parse(cidr)); + else net.addAddress(dev, IPv4Network.parse(cidr)); + } catch (Exception e) { + Log.w(TAG, fmt("Skipping invalid CIDR %s on %s", cidr, dev), e); + } + } + + private void startL3Services() { + firewall.initNetwork(inst); + net.populateRuleRoute(inst); + var watcher = inst.getStore().context.getRouterWatcher(); + if (watcher != null) { + watcher.setForNewNetwork(inst); + // seed the host-IP set DNAT rules are scoped to, then track changes + synchronized (this) { + appliedHostIps.clear(); + appliedHostIps.addAll(watcher.getHostIpv4Addresses()); + } + hostIpListener = this::onHostIpsChanged; + watcher.addHostIpListener(hostIpListener); + } + if (BridgeDhcp.isNeeded(inst.getVlans())) { + dhcp = new BridgeDhcp(inst, this::onPdAddressChanged); + if (!dhcp.start()) + Log.e(TAG, fmt("Failed to start bridgedhcp for %s", br)); + snapshotPdUplinks(); + } + } + + /** Records the uplink each DHCP-PD VLAN resolved to in the running config. */ + private synchronized void snapshotPdUplinks() { + pdConfiguredUplink.clear(); + for (var vlan : inst.getVlans()) { + if (vlan.getIpv6Source() != Ipv6Source.DHCP_PD) continue; + var id = vlan.getPdUplink(); + if (id == null) continue; + var dev = UplinkResolver.resolve(id); + pdConfiguredUplink.put(vlan.getVlanId(), dev == null ? "" : dev); + } + } + + /** + * Reloads bridgedhcp when a DHCP-PD VLAN's uplink now resolves to a + * different device than the running config has — most importantly when it + * was unresolved at start (so PD was skipped) and the interface (e.g. + * Wi-Fi) has since come up. A still-unresolved uplink is left alone: a + * running PD client already retries on its own. Invoked on host-network + * changes, so the freshly-connected uplink gets a PD client. + */ + private synchronized void reconcilePdUplinks() { + if (dhcp == null) return; + boolean changed = false; + for (var vlan : inst.getVlans()) { + if (vlan.getIpv6Source() != Ipv6Source.DHCP_PD) continue; + var id = vlan.getPdUplink(); + if (id == null) continue; + var dev = UplinkResolver.resolve(id); + if (dev == null) continue; // still down: bridgedhcp retries itself + if (!dev.equals(pdConfiguredUplink.get(vlan.getVlanId()))) { + changed = true; + break; + } + } + if (!changed) return; + Log.i(TAG, fmt("PD uplink now available for %s; reloading bridgedhcp", br)); + dhcp.restart(); + snapshotPdUplinks(); + } + + /** + * PD event from bridgedhcp: the helper already plumbed the address on + * the per-VLAN bridge (and DHCPv6/RA follow it automatically); what is + * left is the host side — firewall accept rules for the delegated + * subnet and the policy-routing entries for return traffic. + */ + private synchronized void onPdAddressChanged(int vlanId, @Nullable IPv6Network address) { + var dev = LinuxNetwork.vlanDevice(br, vlanId); + var old = liveV6.remove(vlanId); + if (old != null) + firewall.removeLiveV6Subnet(inst, dev, old); + if (address != null) { + liveV6.put(vlanId, address); + firewall.addLiveV6Subnet(inst, dev, address); + net.populateRouteForNetwork(dev, address); + } + } + + @Override + public void stop() { + if (hostIpListener != null) { + var watcher = inst.getStore().context.getRouterWatcher(); + if (watcher != null) + watcher.removeHostIpListener(hostIpListener); + hostIpListener = null; + } + synchronized (this) { + appliedHostIps.clear(); + } + liveV6.clear(); + if (dhcp != null) { + dhcp.stop(); + dhcp = null; + } + if (pbridge != null) { + pbridge.stop(); + pbridge = null; + } + if (uplinkEnslaved && resolvedUplink != null) { + net.removeInterface(resolvedUplink); + uplinkEnslaved = false; + } + if (inst.getUplinkMode() == UplinkMode.L3) { + firewall.deinitNetwork(inst); + for (var vid : createdVlanBridges) + net.deleteVlanBridge(br, vid); + createdVlanBridges.clear(); + } + net.setLinkState(br, false); + if (net.isInterfaceExists(br) && !net.deleteBridge(br)) + Log.w(TAG, fmt("Failed to delete bridge %s", br)); + macSecurityActive.clear(); + installedForwards.clear(); + forwardFailures.clear(); + } + + @Override + public void attachNic(@NonNull VMNicConfig nic, @NonNull String tapName) throws Exception { + if (net.isInterfaceExists(tapName)) net.deleteTap(tapName); + if (!net.createTap(tapName)) + throw new RuntimeException(fmt("Failed to create TAP %s", tapName)); + try { + // an access port may reference a VLAN the network didn't configure; + // create its per-VLAN bridge on demand so the VM can still boot + if (inst.getUplinkMode() == UplinkMode.L3 && nic.getVlanId() != null) + ensureVlanBridge(nic.getVlanId()); + var target = resolveAttachBridge(nic); + if (!net.addInterface(target, tapName)) + throw new RuntimeException(fmt( + "Failed to add TAP %s to bridge %s", tapName, target)); + // The guest MAC is passed to the hypervisor only. Setting it on + // the host-side tap would duplicate the guest's MAC on the host + // and break L2 forwarding. + if (nic.isIsolated() && !net.setPortIsolated(tapName, true)) + Log.w(TAG, fmt("Failed to isolate port %s", tapName)); + applyMacSecurity(nic, tapName); + if (!net.setLinkState(tapName, true)) + Log.w(TAG, fmt("Failed to bring up TAP %s", tapName)); + if (inst.getUplinkMode() == UplinkMode.L3) { + if (dhcp != null) dhcp.reloadStaticLeases(); + installNicForwards(nic, tapName); + } + } catch (Exception e) { + net.deleteTap(tapName); + throw e; + } + } + + private void applyMacSecurity(@NonNull VMNicConfig nic, @NonNull String tapName) { + if (!nic.isMacSecurity()) return; + var mac = nic.getMacAddress(); + if (mac == null) { + Log.w(TAG, fmt("MAC security on %s skipped: no MAC configured", tapName)); + macSecurityActive.put(tapName, false); + return; + } + // "locked" bridge ports need kernel >= 5.16; degrade with a warning + if (net.setPortLocked(tapName, true) && net.fdbAddStatic(mac, tapName)) { + macSecurityActive.put(tapName, true); + } else { + Log.w(TAG, fmt( + "MAC security unavailable for %s (kernel without locked bridge ports?)", + tapName + )); + net.setPortLocked(tapName, false); + macSecurityActive.put(tapName, false); + } + } + + /** + * Ensures the per-VLAN bridge + trunk leg for an access port's VLAN exist, + * creating them on demand for a VLAN the network config didn't declare + * (an L2-only segment, no gateway/DHCP). Recorded so stop() removes it. + * No-op for the trunk marker (4095) and for 0: a VID-0 leg must never + * be created (it would catch only priority-tagged frames). + */ + private synchronized void ensureVlanBridge(int vid) { + if (vid == 0 || vid == 4095) return; + var perBr = LinuxNetwork.perVlanBridge(br, vid); + if (net.isInterfaceExists(perBr)) { + createdVlanBridges.add(vid); + return; + } + if (!net.createVlanBridge(br, vid)) + Log.w(TAG, fmt("Failed to create on-demand per-VLAN bridge for vid %d", vid)); + createdVlanBridges.add(vid); + } + + /** + * The bridge a NIC's tap attaches to: a trunk port (no VLAN id / the + * 4095 marker) or any non-L3 network joins the main bridge; a tagged + * access port joins its VLAN's per-VLAN bridge. VLAN 0 (untagged-only) + * ports don't exist on a Linux bridge — validation rejects them, since + * without VLAN filtering they could only behave as trunks. + */ + @NonNull + private String resolveAttachBridge(@NonNull VMNicConfig nic) { + if (inst.getUplinkMode() != UplinkMode.L3) return br; + var vlanId = nic.getVlanId(); + if (vlanId == null || vlanId == 4095) return br; + return LinuxNetwork.perVlanBridge(br, vlanId); + } + + private synchronized void installNicForwards(@NonNull VMNicConfig nic, @NonNull String tapName) { + var vlan = nic.resolveDhcpVlan(inst); + if (vlan == null) return; + if (!nic.isDhcp4LeaseEnabled() || !vlan.isDhcp4Enabled() || !vlan.isIpv4Snat()) + return; + var net4 = vlan.getIpv4Network(); + if (net4 == null) return; + String guestIp; + try { + guestIp = net4.addressAtOffset(nic.getDhcp4Offset()).toString(); + } catch (Exception e) { + Log.w(TAG, "Invalid DHCPv4 lease offset, skipping forwards", e); + return; + } + // NAT-loopback: a guest in this subnet reaching the forward gets + // masqueraded so the target's reply returns via the gateway + var hairpinSubnet = net4.toNetworkString(); + var installed = new java.util.ArrayList(); + var failures = new java.util.ArrayList(); + for (var fwd : nic.getDhcp4Forwards()) { + try { + fwd.validate(); + // "any" installs one rule per concrete protocol (tcp+udp) + for (var proto : fwd.protocols()) { + // host-IP-independent half (FORWARD ACCEPT + hairpin) once... + boolean ok = firewall.applyForwardBase( + br, guestIp, proto, fwd.guestStart(), fwd.guestEnd(), hairpinSubnet); + // ...then a DNAT per current host IP (scoped with -d) + for (var hostIp : appliedHostIps) + firewall.applyDnat(br, guestIp, proto, hostIp, + fwd.hostStart(), fwd.hostEnd(), fwd.guestStart(), fwd.guestEnd()); + if (ok) installed.add(new InstalledForward(guestIp, proto, + fwd.hostStart(), fwd.hostEnd(), fwd.guestStart(), fwd.guestEnd(), + hairpinSubnet)); + else failures.add(fmt("%s %s:%s", proto, fwd.host, fwd.guest)); + } + } catch (Exception e) { + failures.add(fmt("%s:%s (%s)", fwd.host, fwd.guest, e.getMessage())); + } + } + if (!installed.isEmpty()) installedForwards.put(tapName, installed); + if (!failures.isEmpty()) { + forwardFailures.put(tapName, failures); + Log.w(TAG, fmt("Forward install failures on %s: %s", tapName, failures)); + } + } + + /** Removes exactly the forwards this tap installed (DNAT per host IP, then base). */ + private synchronized void removeNicForwards(@NonNull String tapName) { + forwardFailures.remove(tapName); + var installed = installedForwards.remove(tapName); + if (installed == null) return; + for (var f : installed) { + for (var hostIp : appliedHostIps) + firewall.removeDnat(br, f.guestIp, f.proto, hostIp, + f.hs, f.he, f.gs, f.ge); + firewall.removeForwardBase(br, f.guestIp, f.proto, f.gs, f.ge, f.hairpin); + } + } + + /** + * Re-scopes DNAT rules when the phone's host IPv4 set changes: adds rules + * for newly-seen addresses, removes them for vanished ones, leaving the + * FORWARD/hairpin rules (host-IP-independent) untouched. + */ + private synchronized void onHostIpsChanged() { + // a host-network change (e.g. Wi-Fi connecting) is also the cue to + // pick up a PD uplink that was unavailable when the network started + reconcilePdUplinks(); + var watcher = inst.getStore().context.getRouterWatcher(); + if (watcher == null) return; + var target = watcher.getHostIpv4Addresses(); + var added = new java.util.ArrayList(); + var removed = new java.util.ArrayList(); + for (var ip : target) + if (!appliedHostIps.contains(ip)) added.add(ip); + for (var ip : appliedHostIps) + if (!target.contains(ip)) removed.add(ip); + if (added.isEmpty() && removed.isEmpty()) return; + for (var entry : installedForwards.values()) + for (var f : entry) { + for (var ip : added) + firewall.applyDnat(br, f.guestIp, f.proto, ip, + f.hs, f.he, f.gs, f.ge); + for (var ip : removed) + firewall.removeDnat(br, f.guestIp, f.proto, ip, + f.hs, f.he, f.gs, f.ge); + } + appliedHostIps.clear(); + appliedHostIps.addAll(target); + } + + @Override + public void detachNic(@NonNull VMNicConfig nic, @NonNull String tapName) { + if (inst.getUplinkMode() == UplinkMode.L3) + removeNicForwards(tapName); + net.removeInterface(tapName); + net.deleteTap(tapName); + macSecurityActive.remove(tapName); + if (dhcp != null) dhcp.reloadStaticLeases(); + } + + @Override + public JSONArray listAddresses() { + var arr = net.listAddresses(br); + for (var vlan : inst.getVlans()) { + if (vlan.getVlanId() == 0) continue; // untagged = the main bridge + var sub = net.listAddresses( + LinuxNetwork.perVlanBridge(br, vlan.getVlanId())); + for (int i = 0; i < sub.length(); i++) + arr.put(sub.opt(i)); + } + return arr; + } + + /** + * One row per address with three dimensions — VLAN, bound/unbound, + * configured/auto — over the union, in every mode, of every address the + * network carries: the live addresses on each bridge device (the main + * bridge = VLAN 0, every per-VLAN bridge = its VID) and every configured + * CIDR. A configured address that is also live appears once (configured + + * bound); a configured one not yet on a device is configured + unbound; a + * live one with no config is auto (link-local, RA, the L2 uplink mirror + * pbridge parks on the main bridge, ...). The main bridge is always + * scanned — including L2/none mode, where it is the only device and carries + * that mirror; per-VLAN bridges include on-demand ones, not just configured. + */ + @NonNull + @Override + public JSONArray listAddressEntries() { + var out = new JSONArray(); + // VID -> configured (VMM-managed) CIDRs; VLAN 0 included if declared, + // empty in L2/none mode (getVlans() is L3-only). + var configured = new java.util.LinkedHashMap>(); + for (var vlan : inst.getVlans()) + configured.put(vlan.getVlanId(), configuredCidrs(vlan)); + // Every VID whose L3 device may carry addresses, in a stable order: + // VLAN 0 (the main bridge, always — the L2 mirror lands here), the + // configured VLANs in config order, then any on-demand per-VLAN bridge. + var vids = new java.util.LinkedHashSet(); + vids.add(0); + vids.addAll(configured.keySet()); + vids.addAll(createdVlanBridges); + for (var vid : vids) { + var dev = LinuxNetwork.vlanDevice(br, vid); + // canonical key -> raw "ip/plen" of every live address on the device + var live = new java.util.LinkedHashMap(); + var liveArr = net.listAddresses(dev); + for (int i = 0; i < liveArr.length(); i++) { + var raw = liveArr.optString(i, ""); + if (!raw.isEmpty()) live.put(canonAddr(raw), raw); + } + for (var cidr : configured.getOrDefault(vid, java.util.List.of())) { + boolean bound = live.remove(canonAddr(cidr)) != null; + out.put(addressEntry(cidr, vid, bound, "configured")); + } + // The DHCP-PD host address is owned by the dedicated PD entry (it + // carries the release/renew actions and the Searching state), so + // drop it from the live set instead of re-listing it as "auto". + var pdAddr = liveV6.get(vid); + if (pdAddr != null) live.remove(canonAddr(pdAddr.toString())); + for (var raw : live.values()) + out.put(addressEntry(raw, vid, true, "auto")); + } + // L2 pseudo-bridge: pbridge parks each learned guest address on the + // uplink tagged with the offload magic (so the Wi-Fi firmware answers + // ARP/NS for them). Those tagged addresses are exactly the pseudo- + // bridged guests' IPs — surface them under their own source; the metric + // filter drops the host's own (untagged) uplink addresses. + if (pbridge != null && resolvedUplink != null) { + var guests = net.listAddresses(resolvedUplink, Constants.PBRIDGE_OFFLOAD_MAGIC); + for (int i = 0; i < guests.length(); i++) { + var raw = guests.optString(i, ""); + if (!raw.isEmpty()) + out.put(addressEntry(raw, 0, true, "pbridge_guest")); + } + } + return out; + } + + /** Configured (VMM-managed) CIDRs of one VLAN: primary + secondary v4/v6. */ + @NonNull + private static java.util.List configuredCidrs(@NonNull VlanConfig vlan) { + var list = new java.util.ArrayList(); + var v4 = vlan.getIpv4Cidr(); + if (v4 != null) list.add(v4); + list.addAll(vlan.getIpv4Secondary()); + if (vlan.getIpv6Source() == Ipv6Source.STATIC) { + var v6 = vlan.getIpv6Cidr(); + if (v6 != null) list.add(v6); + } + list.addAll(vlan.getIpv6Secondary()); + return list; + } + + /** Canonical "address/prefix" key so config and live forms compare equal. */ + @NonNull + private static String canonAddr(@NonNull String cidr) { + try { + var n = IPNetwork.parse(cidr); + return fmt("%s/%d", n.address().toString(), n.prefix()); + } catch (Exception e) { + return cidr; + } + } + + @NonNull + @Override + public JSONArray listPdStatus() { + // bridgedhcp's runtime PD state, keyed by VLAN id + var runtime = new java.util.HashMap(); + if (dhcp != null) { + var rt = dhcp.getPdStatus(); + for (int i = 0; i < rt.length(); i++) { + var o = rt.optJSONObject(i); + if (o != null) runtime.put(o.optInt("vlan", -1), o); + } + } + // One row per VLAN configured for DHCP-PD, so the UI always reflects + // the intent: when bridgedhcp has no client for it (the PD uplink was + // unresolved at start), synthesize a "no_uplink" row rather than + // hiding the VLAN entirely. + var out = new JSONArray(); + for (var vlan : inst.getVlans()) { + if (vlan.getIpv6Source() != Ipv6Source.DHCP_PD) continue; + int vid = vlan.getVlanId(); + var rt = runtime.get(vid); + if (rt != null) { + out.put(rt); + continue; + } + // No runtime client: tell apart a genuinely unresolved uplink from + // a helper that just isn't serving this VLAN yet (starting/down). + var id = vlan.getPdUplink(); + var resolvable = id != null && UplinkResolver.resolve(id) != null; + out.put(pdEntry(vid, resolvable ? "searching" : "no_uplink", "")); + } + return out; + } + + @NonNull + private static JSONObject pdEntry(int vlan, @NonNull String state, @NonNull String prefix) { + var o = new JSONObject(); + try { + o.put("vlan", vlan); + o.put("state", state); + o.put("prefix", prefix); + } catch (JSONException ignored) { + } + return o; + } + + @Override + public boolean renewPd(int vlanId) { + return dhcp != null && dhcp.renewPd(vlanId); + } + + @Override + public boolean releasePd(int vlanId) { + return dhcp != null && dhcp.releasePd(vlanId); + } + + @NonNull + @Override + public JSONArray listTools() { + var out = new JSONArray(); + if (pbridge != null) out.put(toolEntry("pbridge", pbridge.isRunning())); + if (dhcp != null) out.put(toolEntry("bridgedhcp", dhcp.isRunning())); + return out; + } + + @Nullable + @Override + public java.util.List toolLog(@NonNull String key) { + if ("pbridge".equals(key) && pbridge != null) return pbridge.getLog(); + if ("bridgedhcp".equals(key) && dhcp != null) return dhcp.getLog(); + return null; + } + + @Override + public JSONArray listInterfaces(VMInstanceStore vms) { + return net.listInterfaces(vms, br); + } + + @Override + public JSONArray listNeighbors() { + var arr = net.listNeighbors(br); + for (var vlan : inst.getVlans()) { + if (vlan.getVlanId() == 0) continue; // untagged = the main bridge + var sub = net.listNeighbors( + LinuxNetwork.perVlanBridge(br, vlan.getVlanId())); + for (int i = 0; i < sub.length(); i++) + arr.put(sub.opt(i)); + } + return arr; + } + + @Override + public JSONArray listDhcpLeases() { + if (dhcp == null) return new JSONArray(); + return dhcp.getLeases(); + } + + @Override + public void appendInfo(@NonNull JSONObject obj) throws JSONException { + if (dhcp != null) { + var status = dhcp.getStatus(); + if (status != null) { + var pdStatus = extractPdStatus(status); + if (pdStatus.length() > 0) obj.put("pd_status", pdStatus); + } + } + if (resolvedUplink != null) obj.put("resolved_uplink", resolvedUplink); + if (!macSecurityActive.isEmpty()) { + var sec = new JSONObject(); + for (var entry : macSecurityActive.entrySet()) + sec.put(entry.getKey(), entry.getValue()); + obj.put("mac_security_active", sec); + } + appendForwardFailures(obj, forwardFailures); + } + + /** + * Converts bridgedhcp's status document into the legacy pd_status + * array shape ({vlan_id, state, prefix}). + */ + @NonNull + private static JSONArray extractPdStatus(@NonNull JSONObject status) { + var out = new JSONArray(); + var ifaces = status.optJSONArray("ifaces"); + if (ifaces == null) return out; + for (int i = 0; i < ifaces.length(); i++) { + var iface = ifaces.optJSONObject(i); + if (iface == null) continue; + var state = iface.optString("pd_state", ""); + if (state.isEmpty()) continue; + try { + var entry = new JSONObject(); + entry.put("vlan_id", Integer.parseInt(iface.optString("tag", "-1"))); + entry.put("state", state); + var prefix = iface.optString("pd_prefix", ""); + if (!prefix.isEmpty()) entry.put("prefix", prefix); + out.put(entry); + } catch (Exception ignored) { + } + } + return out; + } + + @NonNull + @Override + public Map getLiveV6Networks() { + return Map.copyOf(liveV6); + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/LinuxNetwork.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/LinuxNetwork.java index 1ef9c6bc8..2cf5d1449 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/LinuxNetwork.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/LinuxNetwork.java @@ -1,13 +1,12 @@ package cn.classfun.droidvm.daemon.network.backend; -import static cn.classfun.droidvm.lib.utils.FileUtils.readFile; -import static cn.classfun.droidvm.lib.utils.RunUtils.runList; -import static cn.classfun.droidvm.lib.utils.RunUtils.runListQuiet; +import static cn.classfun.droidvm.lib.utils.RunUtils.run; import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; import android.util.Log; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import org.json.JSONArray; import org.json.JSONException; @@ -15,14 +14,20 @@ import java.io.File; import java.util.LinkedHashSet; -import java.util.List; import java.util.Set; import cn.classfun.droidvm.daemon.network.NetworkInstance; import cn.classfun.droidvm.daemon.vm.VMInstanceStore; import cn.classfun.droidvm.lib.network.IPNetwork; import cn.classfun.droidvm.lib.network.IPv4Network; +import cn.classfun.droidvm.lib.network.IPv6Network; +/** + * Linux data-path operations. Every netlink op goes through {@link Netbox} + * (our bundled static rtnetlink CLI) instead of the system {@code ip}/{@code + * bridge}, so behaviour is identical across OEMs. The only direct shell-outs + * left are procfs writes (sysctl) and a sysfs existence check. + */ @SuppressWarnings({"BooleanMethodIsAlwaysInverted", "unused", "UnusedReturnValue"}) public final class LinuxNetwork extends BackendBase { private static final String TAG = "BridgeManager"; @@ -30,80 +35,158 @@ public final class LinuxNetwork extends BackendBase { @Override public boolean createBridge(@NonNull String name) { Log.i(TAG, fmt("Creating bridge: %s", name)); - var result = runList("ip", "link", "add", name, "type", "bridge"); - result.printLog(TAG); - return result.isSuccess(); + return Netbox.linkAddBridge(name); + } + + /** + * Disables IPv6 RA acceptance on a host L3 device. The main bridge, + * per-VLAN bridges and trunk legs are router LAN-side interfaces facing + * the VMs; with the kernel default ({@code accept_ra=1}) a VM emitting + * RAs could make the host autoconfigure an address or default route. + * Best effort (the node is absent when IPv6 is disabled on the device). + */ + public void disableAcceptRa(@NonNull String dev) { + run("echo 0 > /proc/sys/net/ipv6/conf/%s/accept_ra", dev); } @Override public boolean deleteBridge(@NonNull String name) { Log.i(TAG, fmt("Deleting bridge: %s", name)); - var result = runList("ip", "link", "delete", name, "type", "bridge"); - result.printLog(TAG); - return result.isSuccess(); + return Netbox.linkDel(name); + } + + /** base64url alphabet (A-Z a-z 0-9 - _); all legal in interface names. */ + private static final char[] B64URL = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".toCharArray(); + + /** + * Encodes a VLAN id as a fixed 2-char base64url code (the 12-bit VLAN + * space packs into two 6-bit digits), keeping {@code {br}.{code}} / + * {@code {br}v{code}} well within IFNAMSIZ (a 10-char bridge name + 3 = 13). + */ + @NonNull + public static String vlanCode(int vid) { + int v = vid & 0xFFF; + return new String(new char[]{B64URL[(v >> 6) & 0x3F], B64URL[v & 0x3F]}); + } + + /** + * Per-VLAN Linux bridge that carries a tagged VLAN's L3 (gateway + * address, DHCP, SNAT). Stock Android GKI has no bridge VLAN filtering, + * so each tagged VLAN gets its own bridge instead of per-port VIDs on + * one bridge. Never used for VLAN 0: see {@link #vlanDevice}. + */ + @NonNull + public static String perVlanBridge(@NonNull String bridge, int vid) { + return fmt("%sv%s", bridge, vlanCode(vid)); + } + + /** + * The device carrying a VLAN's L3. VLAN 0 is the untagged domain: its + * traffic lives on the main bridge itself (the IP stack has no trunk + * semantics — untagged frames are simply what the bridge device sees), + * so no per-VLAN bridge and no 802.1q leg exist for it. An 802.1q + * subinterface with VID 0 would receive only priority-tagged frames, + * never the untagged traffic it is meant to serve. + */ + @NonNull + public static String vlanDevice(@NonNull String bridge, int vid) { + return vid == 0 ? bridge : perVlanBridge(bridge, vid); + } + + /** + * 802.1q trunk leg: a subinterface on the main bridge (the tagged trunk + * backbone) enslaved into the per-VLAN bridge, so trunk ports on the main + * bridge exchange this VLAN's frames with the per-VLAN bridge's access + * ports while staying isolated from other VLANs. + */ + @NonNull + public static String vlanTrunkLeg(@NonNull String bridge, int vid) { + return fmt("%s.%s", bridge, vlanCode(vid)); + } + + /** + * Brings up the per-VLAN bridge and its trunk leg: creates {br}v{hex}, + * the {br}.{hex} 802.1q subinterface on the main bridge, enslaves the leg + * into the per-VLAN bridge and brings both up. + */ + public boolean createVlanBridge(@NonNull String mainBr, int vid) { + var perBr = perVlanBridge(mainBr, vid); + var leg = vlanTrunkLeg(mainBr, vid); + Log.i(TAG, fmt("Creating per-VLAN bridge %s + trunk leg %s (vid %d)", + perBr, leg, vid)); + boolean ok = createBridge(perBr); + ok &= Netbox.linkAddVlan(mainBr, leg, vid); + ok &= addInterface(perBr, leg); + ok &= setLinkState(leg, true); + ok &= setLinkState(perBr, true); + return ok; + } + + /** Tears down the trunk leg and per-VLAN bridge created by createVlanBridge. */ + public void deleteVlanBridge(@NonNull String mainBr, int vid) { + var perBr = perVlanBridge(mainBr, vid); + var leg = vlanTrunkLeg(mainBr, vid); + if (isInterfaceExists(leg)) Netbox.linkDel(leg); + if (isInterfaceExists(perBr)) deleteBridge(perBr); + } + + public boolean setPortIsolated(@NonNull String dev, boolean isolated) { + return Netbox.linkSetIsolated(dev, isolated); + } + + /** + * Locks a bridge port so only static fdb entries may send (MAC + * security). Requires kernel >= 5.16; callers degrade gracefully. + */ + public boolean setPortLocked(@NonNull String dev, boolean locked) { + return Netbox.linkSetLocked(dev, locked); + } + + public boolean fdbAddStatic(@NonNull String mac, @NonNull String dev) { + return Netbox.fdbAdd(mac, dev); } @Override public boolean createTap(@NonNull String name) { Log.i(TAG, fmt("Creating TAP: %s", name)); - var result = runList( - "ip", "tuntap", "add", "dev", name, "mode", "tap", "vnet_hdr" - ); - result.printLog(TAG); - return result.isSuccess(); + return Netbox.linkAddTap(name); } @Override public boolean deleteTap(@NonNull String name) { Log.i(TAG, fmt("Deleting TAP: %s", name)); - var result = runList("ip", "link", "delete", name); - result.printLog(TAG); - return result.isSuccess(); + return Netbox.linkDel(name); } @Override public boolean setLinkState(@NonNull String name, boolean up) { - var state = up ? "up" : "down"; - Log.i(TAG, fmt("Setting %s %s", name, state)); - var result = runList("ip", "link", "set", name, state); - result.printLog(TAG); - return result.isSuccess(); + Log.i(TAG, fmt("Setting %s %s", name, up ? "up" : "down")); + return Netbox.linkSetState(name, up); } @Override public boolean addInterface(@NonNull String bridge, @NonNull String iface) { Log.i(TAG, fmt("Adding %s to bridge %s", iface, bridge)); - var result = runList("ip", "link", "set", iface, "master", bridge); - result.printLog(TAG); - return result.isSuccess(); + return Netbox.linkSetMaster(iface, bridge); } @Override public boolean removeInterface(@NonNull String iface) { Log.i(TAG, fmt("Removing %s from bridge", iface)); - var result = runList("ip", "link", "set", iface, "nomaster"); - result.printLog(TAG); - return result.isSuccess(); + return Netbox.linkSetNomaster(iface); } @Override public boolean setMacAddress(@NonNull String dev, @NonNull String mac) { Log.i(TAG, fmt("Setting MAC %s on %s", mac, dev)); - var result = runList("ip", "link", "set", "dev", dev, "address", mac); - result.printLog(TAG); - return result.isSuccess(); + return Netbox.linkSetMac(dev, mac); } @Override public boolean setStp(@NonNull String bridge, boolean enabled) { - var val = enabled ? "1" : "0"; - Log.i(TAG, fmt("Setting STP %s on %s", val, bridge)); - var result = runList( - "ip", "link", "set", "dev", bridge, - "type", "bridge", "stp_state", val - ); - result.printLog(TAG); - return result.isSuccess(); + Log.i(TAG, fmt("Setting STP %s on %s", enabled ? "1" : "0", bridge)); + return Netbox.linkSetStp(bridge, enabled); } @Override @@ -114,65 +197,59 @@ public boolean isInterfaceExists(@NonNull String name) { @Override public boolean addAddress(@NonNull String dev, @NonNull IPNetwork cidr) { Log.i(TAG, fmt("Adding address %s to %s", cidr, dev)); - var result = runList("ip", "addr", "add", cidr.toString(), "dev", dev); - result.printLog(TAG); - return result.isSuccess(); + return Netbox.addrAdd(dev, cidr.toString()); } @Override public boolean removeAddress(@NonNull String dev, @NonNull IPNetwork cidr) { Log.i(TAG, fmt("Removing address %s from %s", cidr, dev)); - var result = runList("ip", "addr", "del", cidr.toString(), "dev", dev); - result.printLog(TAG); - return result.isSuccess(); + return Netbox.addrDel(dev, cidr.toString()); } @NonNull @Override public JSONArray listAddresses(@NonNull String dev) { - var addr = new JSONArray(); - var result = runListQuiet("ip", "-j", "addr", "show", "dev", dev); - if (!result.isSuccess()) return addr; - try { - var arr = new JSONArray(result.getOutString()); - for (int i = 0; i < arr.length(); i++) { - var link = arr.getJSONObject(i); - var addrInfo = link.optJSONArray("addr_info"); - if (addrInfo == null) continue; - for (int j = 0; j < addrInfo.length(); j++) { - var ai = addrInfo.getJSONObject(j); - var local = ai.optString("local", ""); - var prefixLen = ai.optInt("prefixlen", 0); - if (!local.isEmpty()) - addr.put(fmt("%s/%d", local, prefixLen)); - } - } - } catch (JSONException e) { - Log.e(TAG, fmt("Failed to parse addresses of %s", dev), e); + return listAddresses(dev, null); + } + + /** + * Live addresses on {@code dev} as {@code "ip/plen"} strings; when + * {@code metric} is non-null, only addresses tagged with that + * IFA_RT_PRIORITY (pbridge's offload-proxy magic) are returned. + */ + @NonNull + public JSONArray listAddresses(@NonNull String dev, @Nullable Long metric) { + var out = new JSONArray(); + var rows = Netbox.addrList(dev, metric); + for (int i = 0; i < rows.length(); i++) { + var r = rows.optJSONObject(i); + if (r == null) continue; + var local = r.optString("local", ""); + if (!local.isEmpty()) + out.put(fmt("%s/%d", local, r.optInt("prefixlen", 0))); } - return addr; + return out; } @NonNull @Override public JSONArray listBridges() { - var result = runListQuiet("ip", "-j", "link", "show", "type", "bridge"); var bridges = new JSONArray(); - if (!result.isSuccess()) return bridges; - try { - var arr = new JSONArray(result.getOutString()); - for (int i = 0; i < arr.length(); i++) { - var link = arr.getJSONObject(i); + var rows = Netbox.linkList(null, true); + for (int i = 0; i < rows.length(); i++) { + var r = rows.optJSONObject(i); + if (r == null) continue; + var name = r.optString("ifname", ""); + try { var br = new JSONObject(); - var name = link.optString("ifname", ""); br.put("name", name); - br.put("state", link.optString("operstate", "UNKNOWN").toUpperCase()); - br.put("mtu", link.optInt("mtu", 1500)); + br.put("state", r.optString("operstate", "UNKNOWN").toUpperCase()); + br.put("mtu", r.optInt("mtu", 1500)); br.put("interfaces", listBridgeInterfaces(name)); bridges.put(br); + } catch (JSONException e) { + Log.e(TAG, fmt("Failed to build bridge entry for %s", name), e); } - } catch (JSONException e) { - Log.e(TAG, "Failed to parse bridge list", e); } return bridges; } @@ -181,16 +258,10 @@ public JSONArray listBridges() { @Override public JSONArray listBridgeInterfaces(@NonNull String bridge) { var members = new JSONArray(); - var result = runListQuiet("ip", "-j", "link", "show", "master", bridge); - if (!result.isSuccess()) return members; - try { - var arr = new JSONArray(result.getOutString()); - for (int i = 0; i < arr.length(); i++) { - var link = arr.getJSONObject(i); - members.put(link.optString("ifname", "")); - } - } catch (JSONException e) { - Log.e(TAG, fmt("Failed to parse interfaces of %s", bridge), e); + var rows = Netbox.linkList(bridge, false); + for (int i = 0; i < rows.length(); i++) { + var r = rows.optJSONObject(i); + if (r != null) members.put(r.optString("ifname", "")); } return members; } @@ -199,25 +270,22 @@ public JSONArray listBridgeInterfaces(@NonNull String bridge) { @Override public JSONArray listAvailableInterfaces() { var iface = new JSONArray(); - var result = runListQuiet("ip", "-j", "link", "show"); - if (!result.isSuccess()) return iface; - try { - var arr = new JSONArray(result.getOutString()); - for (int i = 0; i < arr.length(); i++) { - var link = arr.getJSONObject(i); - var name = link.optString("ifname", ""); - if (name.equals("lo")) continue; - var info = link.optJSONObject("linkinfo"); - if (info != null && info.optString("info_kind", "").equals("bridge")) - continue; + var rows = Netbox.linkList(null, false); + for (int i = 0; i < rows.length(); i++) { + var r = rows.optJSONObject(i); + if (r == null) continue; + var name = r.optString("ifname", ""); + if (name.equals("lo")) continue; + if (r.optString("kind", "").equals("bridge")) continue; + try { var obj = new JSONObject(); obj.put("name", name); - obj.put("state", link.optString("operstate", "UNKNOWN").toUpperCase()); - obj.put("master", link.optString("master", "")); + obj.put("state", r.optString("operstate", "UNKNOWN").toUpperCase()); + obj.put("master", r.optString("master", "")); iface.put(obj); + } catch (JSONException e) { + Log.e(TAG, "Failed to build interface entry", e); } - } catch (JSONException e) { - Log.e(TAG, "Failed to parse interface list", e); } return iface; } @@ -226,32 +294,25 @@ public JSONArray listAvailableInterfaces() { @Override public JSONArray listInterfaces(VMInstanceStore vms, String bridge) { var iFaces = new JSONArray(); - try { - var result = runListQuiet("ip", "-j", "link", "show", "master", bridge); - if (!result.isSuccess()) { - Log.w(TAG, fmt("Failed to list neighbors for bridge %s", bridge)); - result.printLog("ip-neigh"); - return iFaces; - } - var data = result.getOutString(); - if (data.isEmpty()) return iFaces; - var arr = new JSONArray(data); - for (int i = 0; i < arr.length(); i++) { - var link = arr.getJSONObject(i); + var rows = Netbox.linkList(bridge, false); + for (int i = 0; i < rows.length(); i++) { + var r = rows.optJSONObject(i); + if (r == null) continue; + var ifname = r.optString("ifname", ""); + try { var obj = new JSONObject(); - var ifname = link.optString("ifname", ""); obj.put("name", ifname); - obj.put("address", link.optString("address", "")); - obj.put("state", link.optString("operstate", "UNKNOWN")); + obj.put("address", r.optString("address", "")); + obj.put("state", r.optString("operstate", "UNKNOWN")); var vmInfo = vms.findVMByTap(ifname); if (vmInfo != null) { obj.put("vm_id", vmInfo.optString("vm_id", "")); obj.put("vm_name", vmInfo.optString("vm_name", "")); } iFaces.put(obj); + } catch (Exception e) { + Log.w(TAG, fmt("Error building interface entry for %s", ifname), e); } - } catch (Exception e) { - Log.w(TAG, fmt("Error while list interfaces for bridge %s", bridge), e); } return iFaces; } @@ -259,95 +320,53 @@ public JSONArray listInterfaces(VMInstanceStore vms, String bridge) { @NonNull @Override public JSONArray listNeighbors(String bridge) { - var neighbors = new JSONArray(); - try { - var result = runListQuiet("ip", "neigh", "show", "dev", bridge); - if (!result.isSuccess()) { - Log.w(TAG, fmt("Failed to list neighbors for bridge %s", bridge)); - result.printLog("ip-neigh"); - return neighbors; - } - var data = result.getOutString().trim(); - if (data.isEmpty()) return neighbors; - for (var line : data.split("\n")) { - var parts = line.trim().split("\\s+"); - if (parts.length < 1) continue; - var obj = new JSONObject(); - obj.put("dst", parts[0]); - for (int i = 1; i < parts.length - 1; i++) { - if (parts[i].equals("lladdr")) - obj.put("lladdr", parts[++i]); - else if (parts[i].equals("dev")) - obj.put("dev", parts[++i]); - } - obj.put("state", List.of(parts[parts.length - 1])); - neighbors.put(obj); - } - } catch (Exception e) { - Log.w(TAG, fmt("Error while list neighbors for bridge %s", bridge), e); - } - return neighbors; - } - - @NonNull - @Override - public JSONArray listDhcpLeases(String bridge) { - var leases = new JSONArray(); - var leaseFile = new File(Dnsmasq.getDnsmasqLeaseFile(bridge)); - if (!leaseFile.exists()) { - Log.w(TAG, fmt("DHCP lease file not found for bridge %s: %s", bridge, leaseFile)); - return leases; - } - try { - var leaseResult = readFile(leaseFile); - for (var line : leaseResult.split("\n")) { - if (line.trim().isEmpty()) continue; - var parts = line.trim().split("\\s+"); - if (parts.length >= 4) { - var lease = new JSONObject(); - lease.put("expires", parts[0]); - lease.put("mac", parts[1]); - lease.put("ip", parts[2]); - lease.put("hostname", parts.length >= 5 ? parts[3] : ""); - leases.put(lease); - } - } - } catch (Exception e) { - Log.w(TAG, fmt("Error while read DHCP leases %s", leaseFile), e); - } - return leases; + // netbox's neigh-list already emits {dst,lladdr,dev,state[]}. + return Netbox.neighList(bridge); } + /** Routing-table ids consulted by any "lookup" policy rule, for {@link #populateRouteForNetwork}. */ @NonNull - private Set listRouteTables() { + public Set listRouteTables(boolean ipv6) { var set = new LinkedHashSet(); - var result = runListQuiet("ip", "rule", "show"); - if (!result.isSuccess()) { - Log.w(TAG, "Failed to list IP rules"); - result.printLog("ip-rule"); - return set; - } - var data = result.getOutString(); - for (var line : data.split("\n")) { - var idx = line.indexOf(" lookup "); - if (idx < 0) continue; - set.add(line.substring(idx + (" lookup ".length())).trim()); + var rows = Netbox.ruleList(ipv6); + for (int i = 0; i < rows.length(); i++) { + var r = rows.optJSONObject(i); + if (r == null || !r.optBoolean("lookup", false)) continue; + set.add(String.valueOf(r.optInt("table", 0))); } return set; } + /** Adds one subnet to every policy routing table so host replies route back. */ + public void populateRouteForNetwork(@NonNull String dev, @NonNull IPNetwork net) { + boolean ipv6 = net instanceof IPv6Network; + var spec = net.toNetworkString(); + for (var table : listRouteTables(ipv6)) + Netbox.routeAdd(dev, spec, table, ipv6); + } + @Override public void populateRuleRoute(@NonNull NetworkInstance inst) { - var br = inst.item.optString("bridge_name", ""); - var list = listRouteTables(); - inst.item.get("ipv4_addresses").forEachArray(a -> { - try { - var addr = IPv4Network.parse(a.asString()); - var net = addr.network(); - for (var table : list) - runList("ip", "route", "add", net.toString(), "dev", br, "table", table); - } catch (Exception ignored) { + var br = inst.getBridgeName(); + if (br == null) return; + for (var vlan : inst.getVlans()) { + var dev = vlanDevice(br, vlan.getVlanId()); + var net4 = vlan.getIpv4Network(); + if (net4 != null) populateRouteForNetwork(dev, net4); + for (var cidr : vlan.getIpv4Secondary()) { + try { + populateRouteForNetwork(dev, IPv4Network.parse(cidr)); + } catch (Exception ignored) { + } } - }); + var net6 = vlan.getIpv6Network(); + if (net6 != null) populateRouteForNetwork(dev, net6); + for (var cidr : vlan.getIpv6Secondary()) { + try { + populateRouteForNetwork(dev, IPv6Network.parse(cidr)); + } catch (Exception ignored) { + } + } + } } } diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/ManagedProcess.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/ManagedProcess.java new file mode 100644 index 000000000..c756d4e2e --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/ManagedProcess.java @@ -0,0 +1,196 @@ +package cn.classfun.droidvm.daemon.network.backend; + +import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; + +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.InterruptedIOException; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import cn.classfun.droidvm.lib.utils.ProcessUtils; + +/** + * Supervises one external helper daemon (dnsmasq, gvswitch, pbridge): + * spawns it in the foreground, pumps its output to the log, tracks + * liveness/exit code and stops it with SIGTERM then SIGKILL. + */ +public class ManagedProcess { + public interface LineListener { + void onLine(@NonNull String line); + } + + private final String tag; + private final String logTag; + private Process process = null; + private Thread monitor = null; + private volatile boolean running = false; + private volatile int exitCode = -1; + private Runnable onUnexpectedExit = null; + private volatile LineListener lineListener = null; + /** Recent merged stdout+stderr lines, surfaced to the network info UI. */ + private static final int LOG_CAP = 1000; + private final ArrayDeque logBuffer = new ArrayDeque<>(); + + public ManagedProcess(@NonNull String name, @NonNull String instance) { + this.tag = fmt("%s-%s", name, instance); + this.logTag = tag.length() > 23 ? tag.substring(0, 23) : tag; + } + + /** Called from the monitor thread when the process dies without stop(). */ + public void setOnUnexpectedExit(@Nullable Runnable callback) { + onUnexpectedExit = callback; + } + + /** + * Receives every output line from the monitor thread (in addition to + * logging) — used for child event streams (e.g. bridgedhcp JSON lines). + */ + public void setLineListener(@Nullable LineListener listener) { + lineListener = listener; + } + + public synchronized boolean start(@NonNull List args) { + stop(); + Log.i(logTag, fmt("Starting: %s", String.join(" ", args))); + appendLog(fmt("=== starting: %s ===", String.join(" ", args))); + try { + var pb = new ProcessBuilder(args); + pb.redirectErrorStream(true); + exitCode = -1; + process = pb.start(); + } catch (Exception e) { + Log.e(logTag, "Failed to start process", e); + process = null; + running = false; + return false; + } + running = true; + monitor = new Thread(this::monitorThread, fmt("mon-%s", tag)); + monitor.setDaemon(true); + monitor.start(); + return true; + } + + private void monitorThread() { + var proc = process; + if (proc == null) return; + try { + try (var reader = new BufferedReader( + new InputStreamReader(proc.getInputStream()))) { + String line; + while ((line = reader.readLine()) != null) { + Log.d(logTag, line); + appendLog(line); + var listener = lineListener; + if (listener != null) { + try { + listener.onLine(line); + } catch (Exception e) { + Log.w(logTag, "Line listener failed", e); + } + } + } + } + int code = proc.waitFor(); + exitCode = code; + boolean unexpected = running; + running = false; + appendLog(fmt("=== process exited (code %d) ===", code)); + if (unexpected) { + Log.w(logTag, fmt("Process exited unexpectedly (code %d)", code)); + var cb = onUnexpectedExit; + if (cb != null) cb.run(); + } else { + Log.i(logTag, fmt("Process exited (code %d)", code)); + } + } catch (InterruptedException | InterruptedIOException e) { + Thread.currentThread().interrupt(); + Log.d(logTag, "Process monitor interrupted"); + } catch (Exception e) { + Log.e(logTag, "Process monitor failed", e); + } finally { + running = false; + } + } + + public synchronized void stop() { + running = false; + if (monitor != null) { + monitor.interrupt(); + monitor = null; + } + var proc = process; + process = null; + if (proc == null) return; + Log.i(logTag, "Stopping process"); + proc.destroy(); + try { + proc.waitFor(3, TimeUnit.SECONDS); + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); + } + proc.destroyForcibly(); + try { + proc.waitFor(); + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); + } + } + + /** Sends a signal (e.g. SIGHUP for config reload) to the running process. */ + public synchronized boolean signal(int signal) { + var proc = process; + if (proc == null || !proc.isAlive()) return false; + int pid = pidOf(proc); + if (pid <= 0) return false; + return ProcessUtils.shellKillProcess(pid, signal); + } + + /** java.lang.Process.pid() is not in the Android compile SDK; reflect. */ + private static int pidOf(@NonNull Process proc) { + try { + var method = Process.class.getMethod("pid"); + return (int) (long) (Long) method.invoke(proc); + } catch (Exception ignored) { + } + try { + var field = proc.getClass().getDeclaredField("pid"); + field.setAccessible(true); + return field.getInt(proc); + } catch (Exception e) { + Log.w("ManagedProcess", "Failed to determine process pid", e); + return -1; + } + } + + private void appendLog(@NonNull String line) { + synchronized (logBuffer) { + while (logBuffer.size() >= LOG_CAP) logBuffer.removeFirst(); + logBuffer.addLast(line); + } + } + + /** Snapshot of the last {@value #LOG_CAP} captured output lines. */ + @NonNull + public List getLog() { + synchronized (logBuffer) { + return new ArrayList<>(logBuffer); + } + } + + public boolean isRunning() { + return running && process != null && process.isAlive(); + } + + public int getExitCode() { + return exitCode; + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/Netbox.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/Netbox.java new file mode 100644 index 000000000..7f68754c3 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/Netbox.java @@ -0,0 +1,279 @@ +package cn.classfun.droidvm.daemon.network.backend; + +import static cn.classfun.droidvm.lib.utils.AssetUtils.getAssetBinaryPath; +import static cn.classfun.droidvm.lib.utils.RunUtils.runListQuiet; +import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; + +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; + +/** + * The single entry point for every netlink operation the daemon performs. + * + *

The daemon used to shell out to the system {@code ip}/{@code bridge} + * (iproute2), which vary per OEM — some are too old to even emit + * {@code IFA_RT_PRIORITY} in {@code -j} JSON, the field pbridge tags its + * offload-proxy addresses with. {@code netbox} is our bundled, static Rust tool + * that does the same operations straight over rtnetlink with a fixed JSON + * schema, so behaviour is identical on every device. All argv building, binary + * resolution and JSON parsing live here; callers use the typed methods only. + * + *

Mutations return {@code true} on success (the tool's exit code); queries + * return the tool's parsed JSON ({@link JSONArray}, empty on any failure). + */ +final class Netbox { + private static final String TAG = "Netbox"; + + private Netbox() { + } + + // ---------------------------------------------------------- link mutations + + static boolean linkAddBridge(@NonNull String name) { + return run("link-add-bridge", name); + } + + static boolean linkAddVlan(@NonNull String parent, @NonNull String name, int vid) { + return run("link-add-vlan", "--link", parent, "--name", name, "--id", String.valueOf(vid)); + } + + static boolean linkAddTap(@NonNull String name) { + return run("link-add-tap", name); + } + + static boolean linkDel(@NonNull String name) { + return run("link-del", name); + } + + static boolean linkSetState(@NonNull String name, boolean up) { + return run("link-set-state", name, up ? "up" : "down"); + } + + static boolean linkSetMaster(@NonNull String iface, @NonNull String master) { + return run("link-set-master", iface, master); + } + + static boolean linkSetNomaster(@NonNull String iface) { + return run("link-set-nomaster", iface); + } + + static boolean linkSetMac(@NonNull String dev, @NonNull String mac) { + return run("link-set-mac", dev, mac); + } + + static boolean linkSetStp(@NonNull String bridge, boolean on) { + return run("link-set-stp", bridge, on ? "on" : "off"); + } + + static boolean linkSetIsolated(@NonNull String dev, boolean on) { + return run("link-set-isolated", dev, on ? "on" : "off"); + } + + static boolean linkSetLocked(@NonNull String dev, boolean on) { + return run("link-set-locked", dev, on ? "on" : "off"); + } + + // ---------------------------------------------------------- addr mutations + + static boolean addrAdd(@NonNull String dev, @NonNull String cidr) { + return run("addr-add", dev, cidr); + } + + static boolean addrDel(@NonNull String dev, @NonNull String cidr) { + return run("addr-del", dev, cidr); + } + + // ------------------------------------------------------- route/rule/fdb + + static boolean routeAdd(@NonNull String dev, @NonNull String dst, @NonNull String table, boolean v6) { + return run(v6Args("route-add", v6, "--dev", dev, "--dst", dst, "--table", table)); + } + + static boolean ruleAdd(@NonNull String iif, @NonNull String table, boolean v6) { + return run(v6Args("rule-add", v6, "--iif", iif, "--table", table)); + } + + static boolean ruleDel(@NonNull String iif, @NonNull String table, boolean v6) { + return run(v6Args("rule-del", v6, "--iif", iif, "--table", table)); + } + + static boolean fdbAdd(@NonNull String mac, @NonNull String dev) { + return run("fdb-add", mac, dev); + } + + // ---------------------------------------------------------------- queries + + /** Addresses as {@code [{ifname,family,local,prefixlen,scope,metric,noprefixroute}]}. */ + @NonNull + static JSONArray addrList(@Nullable String dev, @Nullable Long metric) { + var args = new ArrayList(); + args.add("addr-list"); + if (dev != null) { + args.add("--dev"); + args.add(dev); + } + if (metric != null) { + args.add("--metric"); + args.add(String.valueOf(metric)); + } + return query(args); + } + + /** Links as {@code [{ifname,address,operstate,master,mtu,kind}]}. */ + @NonNull + static JSONArray linkList(@Nullable String master, boolean bridgeOnly) { + var args = new ArrayList(); + args.add("link-list"); + if (master != null) { + args.add("--master"); + args.add(master); + } + if (bridgeOnly) args.add("--type-bridge"); + return query(args); + } + + /** Neighbours of one device as {@code [{dst,lladdr,dev,state[]}]}. */ + @NonNull + static JSONArray neighList(@NonNull String dev) { + return query(List.of("neigh-list", dev)); + } + + /** Policy rules as {@code [{priority,iif,table,fwmark,fwmask,detached}]}. */ + @NonNull + static JSONArray ruleList(boolean v6) { + return query(v6 ? List.of("rule-list", "--v6") : List.of("rule-list")); + } + + // -------------------------------------------------------------- host IPs + + /** + * The phone's own reachable IPv4 addresses (one-shot). netbox applies the + * whole policy itself: only addresses on interfaces whose name starts with + * one of {@code prefixes} (the Wi-Fi/cellular/VPN/ethernet/tethering + * allowlist), with bridge devices and {@code excludeMetric}-tagged + * (pbridge offload) addresses dropped. Empty on any failure. + */ + @NonNull + static Set hostIpv4(@NonNull List prefixes, @Nullable Long excludeMetric) { + var cmd = new ArrayList(); + cmd.add(getAssetBinaryPath("netbox")); + cmd.addAll(hostIpArgs("host-ips", prefixes, excludeMetric)); + var result = runListQuiet(cmd); + if (!result.isSuccess()) { + Log.w(TAG, fmt("netbox host-ips failed (exit %d): %s", + result.getCode(), result.getErrString())); + return Set.of(); + } + return parseHostIps(result.getOutString()); + } + + /** + * Streams host-IPv4 changes: starts {@code netbox monitor-addr} and calls + * {@code onChange} with the new set on every change (the same policy as + * {@link #hostIpv4}). netbox emits only on an actual change, so the + * callback fires only when the set really moved. Returns the supervised + * process; the caller owns its lifecycle (and should {@code stop()} it). + */ + @NonNull + static ManagedProcess startHostIpMonitor( + @NonNull List prefixes, @Nullable Long excludeMetric, + @NonNull Consumer> onChange + ) { + var cmd = new ArrayList(); + cmd.add(getAssetBinaryPath("netbox")); + cmd.addAll(hostIpArgs("monitor-addr", prefixes, excludeMetric)); + var proc = new ManagedProcess("netbox", "hostip-mon"); + proc.setLineListener(line -> { + var trimmed = line.trim(); + // skip any non-JSON diagnostics merged from stderr + if (trimmed.isEmpty() || trimmed.charAt(0) != '{') return; + onChange.accept(parseHostIps(trimmed)); + }); + proc.start(cmd); + return proc; + } + + @NonNull + private static List hostIpArgs( + @NonNull String sub, @NonNull List prefixes, @Nullable Long excludeMetric + ) { + var args = new ArrayList(); + args.add(sub); + for (var p : prefixes) { + args.add("--iface"); + args.add(p); + } + if (excludeMetric != null) { + args.add("--exclude-metric"); + args.add(String.valueOf(excludeMetric)); + } + return args; + } + + /** Parse one {@code {"v4":[...]}} line into a set; empty on any failure. */ + @NonNull + private static Set parseHostIps(@NonNull String json) { + var out = new LinkedHashSet(); + try { + var arr = new JSONObject(json).optJSONArray("v4"); + if (arr != null) + for (int i = 0; i < arr.length(); i++) out.add(arr.getString(i)); + } catch (Exception e) { + Log.w(TAG, fmt("Failed to parse netbox host-ips output: %s", json), e); + } + return out; + } + + // ----------------------------------------------------------------- plumbing + + @NonNull + private static String[] v6Args(@NonNull String sub, boolean v6, @NonNull String... rest) { + var args = new ArrayList(); + args.add(sub); + java.util.Collections.addAll(args, rest); + if (v6) args.add("--v6"); + return args.toArray(new String[0]); + } + + private static boolean run(@NonNull String... args) { + var cmd = new ArrayList(args.length + 1); + cmd.add(getAssetBinaryPath("netbox")); + java.util.Collections.addAll(cmd, args); + var result = runListQuiet(cmd); + if (!result.isSuccess()) { + Log.w(TAG, fmt("netbox %s failed (exit %d): %s", + args[0], result.getCode(), result.getErrString())); + } + return result.isSuccess(); + } + + @NonNull + private static JSONArray query(@NonNull List args) { + var cmd = new ArrayList(args.size() + 1); + cmd.add(getAssetBinaryPath("netbox")); + cmd.addAll(args); + var result = runListQuiet(cmd); + if (!result.isSuccess()) { + Log.w(TAG, fmt("netbox %s failed (exit %d): %s", + args.get(0), result.getCode(), result.getErrString())); + return new JSONArray(); + } + try { + return new JSONArray(result.getOutString()); + } catch (Exception e) { + Log.e(TAG, fmt("Failed to parse netbox %s output", args.get(0)), e); + return new JSONArray(); + } + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/Pbridge.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/Pbridge.java new file mode 100644 index 000000000..bd64792fd --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/Pbridge.java @@ -0,0 +1,94 @@ +package cn.classfun.droidvm.daemon.network.backend; + +import static cn.classfun.droidvm.lib.utils.AssetUtils.getAssetBinaryPath; +import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; + +import android.util.Log; + +import androidx.annotation.NonNull; + +import java.util.List; + +/** + * pseudo-bridge-rs daemon for L2 networks whose uplink cannot be enslaved + * into a Linux bridge (Wi-Fi STA / IFF_DONT_BRIDGE): performs MAC-NAT in + * kernel via eBPF and bridges through a veth pair it manages itself. + */ +public final class Pbridge { + private static final String TAG = "Pbridge"; + private static final int MAX_RESTARTS = 3; + private final ManagedProcess process; + private final String uplink; + private final String bridge; + private int restarts = 0; + private volatile boolean active = false; + + public Pbridge(@NonNull String uplink, @NonNull String bridge) { + this.uplink = uplink; + this.bridge = bridge; + this.process = new ManagedProcess("pbridge", bridge); + this.process.setOnUnexpectedExit(this::onUnexpectedExit); + } + + @NonNull + private List buildArgs() { + return List.of( + getAssetBinaryPath("pbridge"), + "-i", uplink, + "-e", "ebpf", + "-m", "fwd-with-offload", + "-b", bridge, + "--offload-workaround", "v4,v6", + // pin the proxy-address tag so host-IP discovery can exclude them + "--offload-workaround-magic", + String.valueOf(cn.classfun.droidvm.lib.Constants.PBRIDGE_OFFLOAD_MAGIC), + "--arp-keepalive", "10", + "--loglevel", "info" + ); + } + + public boolean start() { + active = true; + restarts = 0; + return process.start(buildArgs()); + } + + public void stop() { + active = false; + process.stop(); + } + + private void onUnexpectedExit() { + if (!active) return; + if (restarts >= MAX_RESTARTS) { + Log.e(TAG, fmt( + "pbridge for %s died %d times, giving up", bridge, restarts + )); + return; + } + restarts++; + Log.w(TAG, fmt( + "Restarting pbridge for %s (attempt %d/%d)", bridge, restarts, MAX_RESTARTS + )); + try { + Thread.sleep(1000L * restarts); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + if (active) process.start(buildArgs()); + } + + public boolean isRunning() { + return process.isRunning(); + } + + public int getExitCode() { + return process.getExitCode(); + } + + @NonNull + public java.util.List getLog() { + return process.getLog(); + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/UplinkResolver.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/UplinkResolver.java new file mode 100644 index 000000000..3557d9909 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/UplinkResolver.java @@ -0,0 +1,251 @@ +package cn.classfun.droidvm.daemon.network.backend; + +import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; + +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.File; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; + +/** + * Enumerates host L2 uplink candidates and resolves the logical + * "WiFi"/"Ethernet"/"Tethering" identifiers to a concrete interface at + * network start: device names drift across reboots (wlan0 ⇄ wlan1), so the + * identifier is stored and re-resolved live; a literal interface name is + * pinned and used as-is. + */ +public final class UplinkResolver { + private static final String TAG = "UplinkResolver"; + public static final String ID_WIFI = "WiFi"; + public static final String ID_ETHERNET = "Ethernet"; + public static final String ID_TETHERING = "Tethering"; + + /** ARPHRD_ETHER: only L2 ethernet-type devices can join a Linux bridge. */ + private static final int ARPHRD_ETHER = 1; + /** + * Android hotspot / USB-tether interface name prefixes. Deliberately NOT + * rmnet: cellular rawip is an L3 device and cannot be bridged. + */ + private static final String[] TETHER_PREFIXES = + {"ap", "swlan", "softap", "rndis", "usb", "bt-pan"}; + + public static final class Iface { + public final String name; + public final boolean wireless, up, ether, physical, leaf, virtual; + + Iface(@NonNull String name, boolean wireless, boolean up, boolean ether, + boolean physical, boolean leaf, boolean virtual) { + this.name = name; + this.wireless = wireless; + this.up = up; + this.ether = ether; + this.physical = physical; + this.leaf = leaf; + this.virtual = virtual; + } + + /** + * A real, non-stacked L2 interface (excludes dummy/ifb/veth/vlan/ + * tunnels): ethernet-type, a leaf netdev (iflink == ifindex), not a + * virtual DEVTYPE, and either up or backed by hardware. A USB-tether + * (rndis) or modem-ethernet has no backing device, so being up admits + * it; a real but unplugged NIC stays via its backing device. + */ + boolean realL2() { + return ether && leaf && !virtual && (up || physical); + } + } + + private UplinkResolver() { + } + + /** All non-loopback, non-bridge, non-tun interfaces with their properties. */ + @NonNull + private static List scan() { + var out = new ArrayList(); + var entries = new File("/sys/class/net").listFiles(); + if (entries == null) return out; + for (var entry : entries) { + var name = entry.getName(); + if (name.equals("lo")) continue; + if (new File(entry, "bridge").exists()) continue; // a bridge + if (new File(entry, "tun_flags").exists()) continue; // tun/tap + boolean wireless = new File(entry, "phy80211").exists() + || new File(entry, "wireless").exists(); + boolean physical = new File(entry, "device").exists(); + boolean ether = readInt(new File(entry, "type")) == ARPHRD_ETHER; + // leaf = not stacked on another netdev (drops veth/vlan/rmnet_data@…) + boolean leaf = readInt(new File(entry, "ifindex")) + == readInt(new File(entry, "iflink")); + out.add(new Iface(name, wireless, isUp(entry), ether, + physical, leaf, isVirtualDevtype(entry))); + } + return out; + } + + private static boolean isUp(@NonNull File entry) { + try { + var s = Files.readAllLines(new File(entry, "operstate").toPath()); + return !s.isEmpty() && s.get(0).trim().equals("up"); + } catch (Exception e) { + return false; + } + } + + private static int readInt(@NonNull File f) { + try { + var s = Files.readAllLines(f.toPath()); + if (s.isEmpty()) return -1; + return Integer.parseInt(s.get(0).trim()); + } catch (Exception e) { + return -1; + } + } + + /** DEVTYPEs that are virtual L2 constructs, never a real uplink. */ + private static final java.util.Set VIRTUAL_DEVTYPES = + java.util.Set.of("vlan", "vxlan", "gretap", "erspan"); + + private static boolean isVirtualDevtype(@NonNull File entry) { + try { + for (var line : Files.readAllLines(new File(entry, "uevent").toPath())) + if (line.startsWith("DEVTYPE=")) + return VIRTUAL_DEVTYPES.contains(line.substring(8).trim()); + } catch (Exception ignored) { + } + return false; + } + + /** + * Concrete physical L2 devices for the picker: ethernet-type, state UP and + * backed by real hardware (excludes br/veth/tap/vlan/vxlan/gretap, which + * have no backing device or are filtered above). + */ + @NonNull + private static List concreteDevices() { + var out = new ArrayList(); + for (var i : scan()) + if (i.realL2() && i.up) out.add(i); + return out; + } + + @Nullable + private static String resolveWifi() { + String fallback = null; + for (var i : scan()) { + if (!i.wireless || !i.leaf || i.virtual) continue; + if (isTetherName(i.name)) continue; // an AP/tether iface, not STA + if (i.up) return i.name; + if (fallback == null) fallback = i.name; + } + return fallback; + } + + @Nullable + private static String resolveEthernet() { + String fallback = null; + for (var i : scan()) { + if (!i.realL2() || i.wireless || isTetherName(i.name)) continue; + if (i.up) return i.name; + if (fallback == null) fallback = i.name; + } + return fallback; + } + + @Nullable + private static String resolveTethering() { + String fallback = null; + for (var i : scan()) { + if (!isTetherName(i.name)) continue; + if (i.up) return i.name; + if (fallback == null) fallback = i.name; + } + return fallback; + } + + private static boolean isTetherName(@NonNull String name) { + for (var p : TETHER_PREFIXES) + if (name.startsWith(p)) return true; + return false; + } + + /** + * Resolves a configured uplink (a logical identifier or a literal + * interface name) to a concrete interface, or null when nothing matches. + */ + @Nullable + public static String resolve(@NonNull String configured) { + if (configured.equalsIgnoreCase(ID_WIFI)) return resolveWifi(); + if (configured.equalsIgnoreCase(ID_ETHERNET)) return resolveEthernet(); + if (configured.equalsIgnoreCase(ID_TETHERING)) return resolveTethering(); + if (new File(fmt("/sys/class/net/%s/uevent", configured)).exists()) + return configured; + Log.w(TAG, fmt("Uplink interface not found: %s", configured)); + return null; + } + + /** Whether a logical identifier's uplink can be bridged (Wi-Fi STA can't). */ + public static boolean identifierBridgeable(@NonNull String id) { + return !id.equalsIgnoreCase(ID_WIFI); + } + + /** + * Whether the interface is assumed to carry IFF_DONT_BRIDGE (not exposed + * via sysfs): a Wi-Fi STA or any non-ethernet-type device cannot be + * enslaved into a Linux bridge, so pseudo-bridging is mandatory. + */ + public static boolean assumeDontBridge(@NonNull String iface) { + var entry = new File("/sys/class/net", iface); + boolean wireless = new File(entry, "phy80211").exists() + || new File(entry, "wireless").exists(); + boolean ether = readInt(new File(entry, "type")) == ARPHRD_ETHER; + return wireless || !ether; + } + + /** + * Picker payload: the three logical identifiers with their live-resolved + * name (empty when none present) and whether they can be bridged, plus the + * concrete physical L2 devices with their per-device bridgeability. + */ + @NonNull + public static JSONObject listUplinksJson() { + var root = new JSONObject(); + try { + var ids = new JSONArray(); + ids.put(identifierJson(ID_WIFI, resolveWifi(), false)); + ids.put(identifierJson(ID_ETHERNET, resolveEthernet(), true)); + ids.put(identifierJson(ID_TETHERING, resolveTethering(), true)); + root.put("identifiers", ids); + var devices = new JSONArray(); + for (var i : concreteDevices()) { + var obj = new JSONObject(); + obj.put("name", i.name); + obj.put("bridgeable", !i.wireless); + devices.put(obj); + } + root.put("devices", devices); + } catch (Exception e) { + Log.w(TAG, "Failed to build uplink list", e); + } + return root; + } + + @NonNull + private static JSONObject identifierJson( + @NonNull String id, @Nullable String name, boolean bridgeable + ) throws org.json.JSONException { + var obj = new JSONObject(); + obj.put("id", id); + obj.put("name", name == null ? "" : name); + obj.put("bridgeable", bridgeable); + return obj; + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/gvisor/GvisorBridgeBackend.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/gvisor/GvisorBridgeBackend.java new file mode 100644 index 000000000..87072d579 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/gvisor/GvisorBridgeBackend.java @@ -0,0 +1,518 @@ +package cn.classfun.droidvm.daemon.network.backend.gvisor; + +import static cn.classfun.droidvm.lib.Constants.DATA_DIR; +import static cn.classfun.droidvm.lib.utils.AssetUtils.getAssetBinaryPath; +import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; +import static cn.classfun.droidvm.lib.utils.StringUtils.pathJoin; + +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; +import java.io.FileWriter; +import java.math.BigInteger; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import cn.classfun.droidvm.daemon.network.NetworkInstance; +import cn.classfun.droidvm.daemon.network.backend.BridgeBackend; +import cn.classfun.droidvm.daemon.network.backend.ManagedProcess; +import cn.classfun.droidvm.daemon.vm.VMInstanceStore; +import cn.classfun.droidvm.lib.store.network.UplinkMode; +import cn.classfun.droidvm.lib.store.network.VlanConfig; +import cn.classfun.droidvm.lib.store.vm.VMNicConfig; + +/** + * gVisor vswitch data path: one gvswitch process per network listening on + * a private unix socket. VM NICs are tap devices that gvswitch takes over + * via AF_XDP; per-VLAN gateways inside gvswitch provide DHCP, SLAAC, SNAT + * (including IPv6) and port forwards. + */ +public final class GvisorBridgeBackend extends BridgeBackend { + private static final String TAG = "GvisorBridgeBackend"; + /** gvswitch forwards take single ports; ranges are expanded with this cap. */ + private static final int MAX_FORWARD_EXPANSION = 128; + private static final SecureRandom random = new SecureRandom(); + private final ManagedProcess process; + private final String id8; + private String token = null; + private GvswitchClient client = null; + /** tapName -> NIC config of currently attached ports. */ + private final Map attached = new ConcurrentHashMap<>(); + /** tapName -> installed gvswitch forward refs ("vlan/id"). */ + private final Map> installedForwards = new ConcurrentHashMap<>(); + /** tapName -> human-readable forward install failures (e.g. port in use). */ + private final Map> forwardFailures = new ConcurrentHashMap<>(); + + public GvisorBridgeBackend(@NonNull NetworkInstance inst) { + super(inst); + this.id8 = inst.getId().toString().substring(0, 8); + this.process = new ManagedProcess("gvswitch", id8); + } + + @NonNull + private String socketPath() { + return pathJoin(DATA_DIR, "run", fmt("gvswitch-%s.sock", id8)); + } + + @NonNull + private String configPath() { + return pathJoin(DATA_DIR, "run", fmt("gvswitch-%s.json", id8)); + } + + @Override + public void start() throws Exception { + var bytes = new byte[16]; + random.nextBytes(bytes); + var sb = new StringBuilder(); + for (var b : bytes) sb.append(fmt("%02x", b)); + token = sb.toString(); + var config = GvswitchConfigBuilder.build(inst); + var configFile = new File(configPath()); + var parent = configFile.getParentFile(); + if (parent != null && !parent.exists() && !parent.mkdirs()) + throw new RuntimeException(fmt("Failed to create %s", parent)); + try (var writer = new FileWriter(configFile)) { + writer.write(config.toString(2)); + } + var sock = new File(socketPath()); + if (sock.exists() && !sock.delete()) + Log.w(TAG, fmt("Failed to remove stale socket %s", sock)); + var args = new ArrayList(); + args.add(getAssetBinaryPath("gvswitch")); + args.add("-listen"); + args.add(socketPath()); + args.add("-auth-token"); + args.add(token); + args.add("-config"); + args.add(configPath()); + if (inst.isStp()) args.add("-stp"); + if (!process.start(args)) + throw new RuntimeException("Failed to start gvswitch"); + client = new GvswitchClient(socketPath(), token); + waitReady(); + } + + private void waitReady() throws Exception { + var deadline = System.currentTimeMillis() + 5000; + Exception last = null; + while (System.currentTimeMillis() < deadline) { + if (!process.isRunning()) + throw new RuntimeException(fmt( + "gvswitch exited during startup (code %d)", process.getExitCode())); + try { + var response = client.get("/api/v1/ports"); + if (response.isSuccess()) return; + last = new RuntimeException(fmt( + "gvswitch API returned %d: %s", response.code, response.body)); + } catch (Exception e) { + last = e; + } + Thread.sleep(200); + } + throw new RuntimeException("gvswitch API did not become ready", last); + } + + @Override + public void stop() { + attached.clear(); + installedForwards.clear(); + forwardFailures.clear(); + process.stop(); + var sock = new File(socketPath()); + if (sock.exists() && !sock.delete()) + Log.w(TAG, fmt("Failed to remove socket %s", sock)); + client = null; + } + + @Override + public void attachNic(@NonNull VMNicConfig nic, @NonNull String tapName) throws Exception { + if (client == null) throw new IllegalStateException("gvswitch not running"); + var net = inst.getStore().backend; + if (net.isInterfaceExists(tapName)) net.deleteTap(tapName); + if (!net.createTap(tapName)) + throw new RuntimeException(fmt("Failed to create TAP %s", tapName)); + try { + if (!net.setLinkState(tapName, true)) + throw new RuntimeException(fmt("Failed to bring up TAP %s", tapName)); + var port = new JSONObject(); + port.put("identifier", tapName); + var vlanId = nic.getVlanId(); + port.put("vlan", vlanId == null ? 4095 : vlanId); + if (nic.isIsolated()) port.put("isolated", true); + var mac = nic.getMacAddress(); + if (nic.isMacSecurity() && mac != null) + port.put("port_security", mac); + port.put("mode", "client"); + port.put("transport", "af_xdp"); + port.put("interface", tapName); + port.put("bpdu_guard", true); + var response = client.post("/api/v1/ports", port); + if (!response.isSuccess()) + throw new RuntimeException(fmt( + "gvswitch port create failed (%d): %s", response.code, response.body)); + attached.put(tapName, nic); + applyStaticLeases(nic, tapName); + installNicForwards(nic, tapName); + } catch (Exception e) { + attached.remove(tapName); + try { + client.delete(fmt("/api/v1/ports/%s", tapName)); + } catch (Exception ignored) { + } + net.deleteTap(tapName); + throw e; + } + } + + private void applyStaticLeases(@NonNull VMNicConfig nic, @NonNull String tapName) + throws Exception { + var vlan = nic.resolveDhcpVlan(inst); + if (vlan == null) return; + var mac = nic.getMacAddress(); + if (nic.isDhcp4LeaseEnabled() && vlan.isDhcp4Enabled() && mac != null) { + var net4 = vlan.getIpv4Network(); + if (net4 != null) { + var binding = new JSONObject(); + binding.put("id", tapName); + binding.put("mac", mac); + binding.put("ip", + net4.addressAtOffset(nic.getDhcp4Offset()).toString()); + check(client.put(fmt( + "/api/v1/gateways/%d/dhcp4/static/%s", vlan.getVlanId(), tapName + ), binding), "dhcp4 static lease"); + } + } + if (nic.isDhcp6LeaseEnabled() && vlan.isDhcp6Enabled()) { + var net6 = vlan.getIpv6Network(); + if (net6 != null) { + var binding = new JSONObject(); + binding.put("id", tapName); + // guest DUIDs are unknown; bind by port (and MAC when present) + binding.put("port_identifier", tapName); + if (mac != null) binding.put("mac", mac); + binding.put("ip", net6.addressAtOffset( + BigInteger.valueOf(nic.getDhcp6Offset())).toString()); + check(client.put(fmt( + "/api/v1/gateways/%d/dhcp6/static/%s", vlan.getVlanId(), tapName + ), binding), "dhcp6 static lease"); + } + } + } + + private void removeStaticLeases(@NonNull VMNicConfig nic, @NonNull String tapName) { + var vlan = nic.resolveDhcpVlan(inst); + if (vlan == null || client == null) return; + try { + client.delete(fmt( + "/api/v1/gateways/%d/dhcp4/static/%s", vlan.getVlanId(), tapName)); + client.delete(fmt( + "/api/v1/gateways/%d/dhcp6/static/%s", vlan.getVlanId(), tapName)); + } catch (Exception e) { + Log.w(TAG, fmt("Failed to remove static leases for %s", tapName), e); + } + } + + /** + * Installs this NIC's port forwards one at a time so a single bind + * failure (host port already in use — gvswitch local forwards listen + * on the host) only drops that one rule. Successfully installed refs + * are recorded per tap so detach removes exactly what this NIC added; + * failures are surfaced via network_info. + */ + private void installNicForwards(@NonNull VMNicConfig nic, @NonNull String tapName) { + var ids = new ArrayList(); + var failures = new ArrayList(); + var vlan = nic.resolveDhcpVlan(inst); + if (vlan != null) { + if (nic.isDhcp4LeaseEnabled() && vlan.isDhcp4Enabled() && vlan.isIpv4Snat()) { + var net4 = vlan.getIpv4Network(); + if (net4 != null) { + var guestIp = guestIpAtOffset(net4, nic.getDhcp4Offset()); + if (guestIp != null) + for (var fwd : nic.getDhcp4Forwards()) + postForward(vlan.getVlanId(), fwd, guestIp, false, ids, failures); + } + } + if (nic.isDhcp6LeaseEnabled() && vlan.isDhcp6Enabled() && vlan.isIpv6Snat()) { + var net6 = vlan.getIpv6Network(); + if (net6 != null) { + var guestIp = guestIpAtOffset(net6, nic.getDhcp6Offset()); + if (guestIp != null) + for (var fwd : nic.getDhcp6Forwards()) + postForward(vlan.getVlanId(), fwd, guestIp, true, ids, failures); + } + } + } + if (!ids.isEmpty()) installedForwards.put(tapName, ids); + if (!failures.isEmpty()) { + forwardFailures.put(tapName, failures); + Log.w(TAG, fmt("Forward install failures on %s: %s", tapName, failures)); + } + } + + @Nullable + private static String guestIpAtOffset( + @NonNull cn.classfun.droidvm.lib.network.IPv4Network net, long offset + ) { + try { + return net.addressAtOffset(offset).toString(); + } catch (Exception e) { + return null; + } + } + + @Nullable + private static String guestIpAtOffset( + @NonNull cn.classfun.droidvm.lib.network.IPv6Network net, long offset + ) { + try { + return net.addressAtOffset(BigInteger.valueOf(offset)).toString(); + } catch (Exception e) { + return null; + } + } + + /** + * POSTs each concrete (proto, port) of one forward; gvswitch takes + * single ports, so ranges are expanded (capped). Each accepted rule's + * "vlan/id" ref is recorded; rejections (e.g. bind in use) are noted. + */ + private void postForward( + int vlanId, @NonNull VMNicConfig.PortForward fwd, @NonNull String guestIp, + boolean v6, @NonNull List ids, @NonNull List failures + ) { + try { + fwd.validate(); + } catch (Exception e) { + failures.add(fmt("%s %s:%s (%s)", fwd.proto, fwd.host, fwd.guest, e.getMessage())); + return; + } + int count = fwd.hostEnd() - fwd.hostStart() + 1; + if (count > MAX_FORWARD_EXPANSION) { + Log.w(TAG, fmt( + "Forward range %s exceeds %d ports on a gVisor bridge, truncating", + fwd.host, MAX_FORWARD_EXPANSION)); + count = MAX_FORWARD_EXPANSION; + } + var bindHost = v6 ? "[::]" : "0.0.0.0"; + var guestHost = v6 ? fmt("[%s]", guestIp) : guestIp; + for (var proto : fwd.protocols()) { + for (int i = 0; i < count; i++) { + int hostPort = fwd.hostStart() + i; + var bind = fmt("%s:%d", bindHost, hostPort); + try { + var rule = new JSONObject(); + rule.put("type", "local"); + rule.put("network", proto); + rule.put("bind", bind); + rule.put("host", fmt("%s:%d", guestHost, fwd.guestStart() + i)); + var resp = client.post( + fmt("/api/v1/gateways/%d/forwards", vlanId), rule); + if (resp.isSuccess()) { + var id = resp.json().optString("id", ""); + if (!id.isEmpty()) ids.add(vlanId + "/" + id); + } else { + failures.add(fmt("%s %s (%s)", proto, bind, briefBody(resp.body))); + } + } catch (Exception e) { + failures.add(fmt("%s %s (%s)", proto, bind, e.getMessage())); + } + } + } + } + + /** Removes exactly the forwards this tap installed. */ + private void removeNicForwards(@NonNull String tapName) { + forwardFailures.remove(tapName); + var ids = installedForwards.remove(tapName); + if (ids == null || client == null) return; + for (var ref : ids) { + int slash = ref.indexOf('/'); + if (slash < 0) continue; + try { + client.delete(fmt("/api/v1/gateways/%s/forwards/%s", + ref.substring(0, slash), ref.substring(slash + 1))); + } catch (Exception e) { + Log.w(TAG, fmt("Failed to delete forward %s", ref), e); + } + } + } + + @NonNull + private static String briefBody(@NonNull String body) { + var b = body.trim(); + return b.length() > 80 ? b.substring(0, 80) : b; + } + + private static void check(@NonNull GvswitchClient.Response response, @NonNull String what) { + if (!response.isSuccess()) throw new RuntimeException(fmt( + "gvswitch %s failed (%d): %s", what, response.code, response.body)); + } + + @Override + public void detachNic(@NonNull VMNicConfig nic, @NonNull String tapName) { + attached.remove(tapName); + if (client != null) { + try { + client.delete(fmt("/api/v1/ports/%s", tapName)); + } catch (Exception e) { + Log.w(TAG, fmt("Failed to delete gvswitch port %s", tapName), e); + } + removeStaticLeases(nic, tapName); + removeNicForwards(tapName); + } + inst.getStore().backend.deleteTap(tapName); + } + + @Override + public JSONArray listAddresses() { + // gateway addresses live inside gvswitch, not on host interfaces + var arr = new JSONArray(); + if (inst.getUplinkMode() != UplinkMode.L3) return arr; + for (var vlan : inst.getVlans()) { + var net4 = vlan.getIpv4Network(); + if (net4 != null) arr.put(net4.toString()); + var net6 = vlan.getIpv6Network(); + if (net6 != null) arr.put(net6.toString()); + } + return arr; + } + + /** + * Same three-dimension shape as the Linux bridge for a consistent UI: the + * gvswitch gateway addresses are the VMM's configured L3 endpoints, so + * every row is configured and (while running) bound. + */ + @NonNull + @Override + public JSONArray listAddressEntries() { + var out = new JSONArray(); + if (inst.getUplinkMode() != UplinkMode.L3) return out; + for (var vlan : inst.getVlans()) { + int vid = vlan.getVlanId(); + var net4 = vlan.getIpv4Network(); + if (net4 != null) out.put(addressEntry(net4.toString(), vid, true, "configured")); + var net6 = vlan.getIpv6Network(); + if (net6 != null) out.put(addressEntry(net6.toString(), vid, true, "configured")); + } + return out; + } + + @Override + public JSONArray listInterfaces(VMInstanceStore vms) { + var arr = new JSONArray(); + if (client == null) return arr; + try { + var response = client.get("/api/v1/ports"); + if (!response.isSuccess()) return arr; + var ports = response.jsonArray(); + for (int i = 0; i < ports.length(); i++) { + var port = ports.getJSONObject(i); + var obj = new JSONObject(); + var name = port.optString("identifier", ""); + obj.put("name", name); + obj.put("state", port.optBoolean("online", false) ? "UP" : "DOWN"); + obj.put("vlan", port.opt("vlan")); + if (vms != null) { + var vmInfo = vms.findVMByTap(name); + if (vmInfo != null) { + obj.put("vm_id", vmInfo.optString("vm_id", "")); + obj.put("vm_name", vmInfo.optString("vm_name", "")); + } + } + arr.put(obj); + } + } catch (Exception e) { + Log.w(TAG, "Failed to list gvswitch ports", e); + } + return arr; + } + + @Override + public JSONArray listNeighbors() { + var arr = new JSONArray(); + if (client == null) return arr; + try { + var response = client.get("/api/v1/fdb"); + if (!response.isSuccess()) return arr; + var entries = response.jsonArray(); + for (int i = 0; i < entries.length(); i++) { + var entry = entries.getJSONObject(i); + var obj = new JSONObject(); + obj.put("lladdr", entry.optString("mac", "")); + obj.put("dev", entry.optString("port", "")); + obj.put("dst", ""); + obj.put("state", List.of( + entry.optBoolean("static", false) ? "STATIC" : "LEARNED")); + arr.put(obj); + } + } catch (Exception e) { + Log.w(TAG, "Failed to list gvswitch fdb", e); + } + return arr; + } + + @Override + public JSONArray listDhcpLeases() { + var arr = new JSONArray(); + if (client == null) return arr; + for (var vlan : inst.getVlans()) { + collectLeases(arr, vlan, "dhcp4"); + collectLeases(arr, vlan, "dhcp6"); + } + return arr; + } + + private void collectLeases(@NonNull JSONArray out, @NonNull VlanConfig vlan, @NonNull String family) { + try { + var response = client.get(fmt( + "/api/v1/gateways/%d/%s/leases", vlan.getVlanId(), family)); + if (!response.isSuccess()) return; + var leases = response.jsonArray(); + for (int i = 0; i < leases.length(); i++) { + var lease = leases.getJSONObject(i); + var obj = new JSONObject(); + obj.put("ip", lease.optString("ip", "")); + obj.put("mac", lease.optString("mac", "")); + obj.put("hostname", lease.optString("port_identifier", "")); + obj.put("expires", lease.optString("expires_at", "")); + obj.put("vlan", vlan.getVlanId()); + out.put(obj); + } + } catch (Exception e) { + Log.d(TAG, fmt("No %s leases for VLAN %d", family, vlan.getVlanId())); + } + } + + @Override + public void appendInfo(@NonNull JSONObject obj) throws JSONException { + obj.put("gvswitch_exit_code", process.getExitCode()); + obj.put("gvswitch_socket", socketPath()); + appendForwardFailures(obj, forwardFailures); + } + + @NonNull + @Override + public JSONArray listTools() { + var out = new JSONArray(); + out.put(toolEntry("gvswitch", process.isRunning())); + return out; + } + + @Nullable + @Override + public java.util.List toolLog(@NonNull String key) { + return "gvswitch".equals(key) ? process.getLog() : null; + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/gvisor/GvswitchClient.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/gvisor/GvswitchClient.java new file mode 100644 index 000000000..2606b0d4c --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/gvisor/GvswitchClient.java @@ -0,0 +1,213 @@ +package cn.classfun.droidvm.daemon.network.backend.gvisor; + +import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; + +import android.net.LocalSocket; +import android.net.LocalSocketAddress; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +/** + * Minimal HTTP/1.1 client for the gvswitch REST API over its unix control + * socket. One request per connection (Connection: close); responses are + * small JSON documents framed by Content-Length. + */ +public final class GvswitchClient { + private final String socketPath; + private final String token; + + public GvswitchClient(@NonNull String socketPath, @NonNull String token) { + this.socketPath = socketPath; + this.token = token; + } + + public static final class Response { + public final int code; + public final String body; + + Response(int code, @NonNull String body) { + this.code = code; + this.body = body; + } + + public boolean isSuccess() { + return code >= 200 && code < 300; + } + + @NonNull + public JSONObject json() throws Exception { + return new JSONObject(body); + } + + @NonNull + public JSONArray jsonArray() throws Exception { + return new JSONArray(body); + } + } + + @NonNull + public Response get(@NonNull String path) throws IOException { + return request("GET", path, null); + } + + @NonNull + public Response post(@NonNull String path, @Nullable Object body) throws IOException { + return request("POST", path, body); + } + + @NonNull + public Response put(@NonNull String path, @Nullable Object body) throws IOException { + return request("PUT", path, body); + } + + @NonNull + public Response delete(@NonNull String path) throws IOException { + return request("DELETE", path, null); + } + + @NonNull + public Response request( + @NonNull String method, @NonNull String path, @Nullable Object body + ) throws IOException { + var payload = body == null + ? new byte[0] + : body.toString().getBytes(StandardCharsets.UTF_8); + try (var socket = new LocalSocket()) { + socket.connect(new LocalSocketAddress( + socketPath, LocalSocketAddress.Namespace.FILESYSTEM)); + socket.setSoTimeout(10_000); + var sb = new StringBuilder(); + sb.append(fmt("%s %s HTTP/1.1\r\n", method, path)); + sb.append("Host: gvswitch\r\n"); + sb.append(fmt("Authorization: Bearer %s\r\n", token)); + sb.append("Connection: close\r\n"); + if (payload.length > 0) + sb.append("Content-Type: application/json\r\n"); + sb.append(fmt("Content-Length: %d\r\n", payload.length)); + sb.append("\r\n"); + var out = socket.getOutputStream(); + out.write(sb.toString().getBytes(StandardCharsets.UTF_8)); + if (payload.length > 0) out.write(payload); + out.flush(); + return readResponse(socket.getInputStream()); + } + } + + @NonNull + private static Response readResponse(@NonNull InputStream in) throws IOException { + var head = readHead(in); + var lines = head.split("\r\n"); + if (lines.length == 0) + throw new IOException("Empty HTTP response"); + var status = lines[0].split(" ", 3); + if (status.length < 2) + throw new IOException(fmt("Bad HTTP status line: %s", lines[0])); + int code; + try { + code = Integer.parseInt(status[1]); + } catch (NumberFormatException e) { + throw new IOException(fmt("Bad HTTP status code: %s", lines[0])); + } + int contentLength = -1; + boolean chunked = false; + for (int i = 1; i < lines.length; i++) { + var idx = lines[i].indexOf(':'); + if (idx < 0) continue; + var name = lines[i].substring(0, idx).trim().toLowerCase(); + var value = lines[i].substring(idx + 1).trim(); + if (name.equals("content-length")) { + try { + contentLength = Integer.parseInt(value); + } catch (NumberFormatException ignored) { + } + } else if (name.equals("transfer-encoding") + && value.toLowerCase().contains("chunked")) { + chunked = true; + } + } + byte[] body; + if (chunked) body = readChunked(in); + else if (contentLength >= 0) body = readN(in, contentLength); + else body = readAll(in); + return new Response(code, new String(body, StandardCharsets.UTF_8)); + } + + @NonNull + private static String readHead(@NonNull InputStream in) throws IOException { + var buf = new ByteArrayOutputStream(); + int state = 0; // counts \r\n\r\n progress + int c; + while ((c = in.read()) >= 0) { + buf.write(c); + if (c == '\r' && (state == 0 || state == 2)) state++; + else if (c == '\n' && (state == 1 || state == 3)) state++; + else state = 0; + if (state == 4) break; + } + var head = buf.toString(StandardCharsets.UTF_8); + return head.endsWith("\r\n\r\n") + ? head.substring(0, head.length() - 4) : head; + } + + @NonNull + private static byte[] readN(@NonNull InputStream in, int n) throws IOException { + var buf = new byte[n]; + int off = 0; + while (off < n) { + int read = in.read(buf, off, n - off); + if (read < 0) break; + off += read; + } + return buf; + } + + @NonNull + private static byte[] readAll(@NonNull InputStream in) throws IOException { + var buf = new ByteArrayOutputStream(); + var chunk = new byte[4096]; + int read; + while ((read = in.read(chunk)) >= 0) + buf.write(chunk, 0, read); + return buf.toByteArray(); + } + + @NonNull + private static byte[] readChunked(@NonNull InputStream in) throws IOException { + var out = new ByteArrayOutputStream(); + while (true) { + var line = readLine(in); + int size; + try { + var semi = line.indexOf(';'); + size = Integer.parseInt( + (semi >= 0 ? line.substring(0, semi) : line).trim(), 16); + } catch (NumberFormatException e) { + throw new IOException(fmt("Bad chunk size: %s", line)); + } + if (size == 0) break; + out.write(readN(in, size)); + readLine(in); // trailing CRLF + } + return out.toByteArray(); + } + + @NonNull + private static String readLine(@NonNull InputStream in) throws IOException { + var buf = new StringBuilder(); + int c; + while ((c = in.read()) >= 0) { + if (c == '\n') break; + if (c != '\r') buf.append((char) c); + } + return buf.toString(); + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/gvisor/GvswitchConfigBuilder.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/gvisor/GvswitchConfigBuilder.java new file mode 100644 index 000000000..8fc753eaf --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/gvisor/GvswitchConfigBuilder.java @@ -0,0 +1,123 @@ +package cn.classfun.droidvm.daemon.network.backend.gvisor; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.math.BigInteger; +import java.util.List; + +import cn.classfun.droidvm.lib.store.network.NetworkConfig; +import cn.classfun.droidvm.lib.store.network.UplinkMode; +import cn.classfun.droidvm.lib.store.network.VlanConfig; + +/** + * Generates the gvswitch startup config (gateways only; VM ports, static + * leases and forwards are managed over the REST API as VMs attach). + */ +final class GvswitchConfigBuilder { + private GvswitchConfigBuilder() { + } + + @NonNull + static JSONObject build(@NonNull NetworkConfig config) throws JSONException { + var root = new JSONObject(); + if (config.getUplinkMode() != UplinkMode.L3) return root; + var gateways = new JSONArray(); + var bridgeMac = config.getBridgeMacAddress(); + for (var vlan : config.getVlans()) { + var gw = buildGateway(vlan, vlan.isUntagged() ? bridgeMac : null); + if (gw != null) gateways.put(gw); + } + if (gateways.length() > 0) root.put("gateways", gateways); + return root; + } + + @Nullable + private static JSONObject buildGateway( + @NonNull VlanConfig vlan, @Nullable String mac + ) throws JSONException { + var net4 = vlan.getIpv4Network(); + var net6 = vlan.getIpv6Network(); + if (net4 == null && net6 == null) return null; + var gw = new JSONObject(); + gw.put("vlan", vlan.getVlanId()); + if (mac != null) gw.put("mac", mac); + // gvswitch has a single internet-routing switch for both families; + // per-family SNAT toggles are expressed via deny ranges + boolean snat4 = net4 != null && vlan.isIpv4Snat(); + boolean snat6 = net6 != null && vlan.isIpv6Snat(); + gw.put("enable_internet_routing", snat4 || snat6); + var deny = new JSONArray(); + if (!snat4 && snat6) deny.put("0.0.0.0/0"); + if (!snat6 && snat4 && net6 != null) deny.put("::/0"); + if (deny.length() > 0) gw.put("deny", deny); + // No explicit DNS: gvswitch advertises the gateway IP via DHCP and + // the dns_proxy answers it through the Android system resolver + // (dnsproxyd/resnsend), so guest lookups behave like the host's + if (vlan.getDnsServers().isEmpty()) gw.put("dns_proxy", true); + if (net4 != null) { + var v4 = new JSONObject(); + v4.put("address", net4.address().toString()); + v4.put("prefix_len", net4.prefix()); + gw.put("ipv4", v4); + if (vlan.isDhcp4Enabled()) { + var dhcp = new JSONObject(); + dhcp.put("enabled", true); + dhcp.put("pool_start", + net4.addressAtOffset(vlan.getDhcp4OffsetStart()).toString()); + dhcp.put("pool_end", + net4.addressAtOffset(vlan.getDhcp4OffsetEnd()).toString()); + var dns = dnsOf(vlan.getDnsServers(), false); + if (dns.length() > 0) dhcp.put("dns", dns); + gw.put("dhcp4", dhcp); + } + } + if (net6 != null) { + var v6 = new JSONObject(); + v6.put("address", net6.address().toString()); + v6.put("prefix_len", net6.prefix()); + gw.put("ipv6", v6); + if (vlan.isDhcp6Enabled()) { + var dhcp = new JSONObject(); + dhcp.put("enabled", true); + dhcp.put("pool_start", net6.addressAtOffset( + BigInteger.valueOf(vlan.getDhcp6OffsetStart())).toString()); + dhcp.put("pool_end", net6.addressAtOffset( + BigInteger.valueOf(vlan.getDhcp6OffsetEnd())).toString()); + var dns = dnsOf(vlan.getDnsServers(), true); + if (dns.length() > 0) dhcp.put("dns", dns); + gw.put("dhcp6", dhcp); + } + if (vlan.isSlaacEnabled()) { + var slaac = new JSONObject(); + slaac.put("enabled", true); + slaac.put("managed", vlan.isDhcp6Enabled()); + slaac.put("other", vlan.isDhcp6Enabled()); + var prefix = new JSONObject(); + prefix.put("prefix", net6.toNetworkString()); + prefix.put("on_link", true); + prefix.put("autonomous", true); + var prefixes = new JSONArray(); + prefixes.put(prefix); + slaac.put("prefixes", prefixes); + gw.put("slaac", slaac); + } + } + return gw; + } + + @NonNull + private static JSONArray dnsOf(@NonNull List servers, boolean v6) { + // empty list = omitted = gvswitch advertises the gateway IP, which + // the backend serves by forwarding gateway:53 to the Android + // system's DNS (see GvisorBridgeBackend.dnsForwardRules) + var arr = new JSONArray(); + for (var s : servers) + if (s.contains(":") == v6) arr.put(s); + return arr; + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/iptables/ChainInfo.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/iptables/ChainInfo.java index 239eaa33a..053e81876 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/iptables/ChainInfo.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/iptables/ChainInfo.java @@ -8,12 +8,18 @@ final class ChainInfo { final IptablesBackend backend; + final boolean ipv6; final String table; final String parent; final String chain; ChainInfo(IptablesBackend backend, String table, String parent, String chain) { + this(backend, false, table, parent, chain); + } + + ChainInfo(IptablesBackend backend, boolean ipv6, String table, String parent, String chain) { this.backend = backend; + this.ipv6 = ipv6; this.table = table; this.parent = parent; this.chain = chain; @@ -21,64 +27,60 @@ final class ChainInfo { ChainInfo(IptablesBackend backend, @NonNull ChainInfo parent, String chain) { this.backend = backend; + this.ipv6 = parent.ipv6; this.table = parent.table; this.parent = parent.chain; this.chain = chain; } + @NonNull + String binary() { + return ipv6 ? "ip6tables" : "iptables"; + } + public void init(boolean insert) { - backend.iptablesCmd(table, "-N", chain); - backend.iptablesCmd(table, "-F", chain); + backend.iptablesCmd(binary(), table, "-N", chain); + backend.iptablesCmd(binary(), table, "-F", chain); if (parent != null && chain != null) { if (insert) - backend.iptablesCmd(table, "-I", parent, "1", "-j", chain); + backend.iptablesCmd(binary(), table, "-I", parent, "1", "-j", chain); else - backend.iptablesCmd(table, "-A", parent, "-j", chain); + backend.iptablesCmd(binary(), table, "-A", parent, "-j", chain); } } public void deinit() { if (table != null && parent != null && chain != null) - backend.iptablesCmd(table, "-D", parent, "-j", chain); + backend.iptablesCmd(binary(), table, "-D", parent, "-j", chain); if (table != null && chain != null) { - backend.iptablesCmd(table, "-F", chain); - backend.iptablesCmd(table, "-X", chain); + backend.iptablesCmd(binary(), table, "-F", chain); + backend.iptablesCmd(binary(), table, "-X", chain); } } - public boolean append(@NonNull String... args) { + private boolean rule(@NonNull String op, @Nullable String pos, @NonNull String... args) { var cmd = new ArrayList(); - cmd.add("iptables"); + cmd.add(binary()); cmd.add("-t"); cmd.add(table); - cmd.add("-A"); + cmd.add(op); cmd.add(chain); + if (pos != null) cmd.add(pos); cmd.addAll(Arrays.asList(args)); return backend.iptables(cmd.toArray(new String[0])); } + public boolean append(@NonNull String... args) { + return rule("-A", null, args); + } + public boolean insert(@NonNull String... args) { - var cmd = new ArrayList(); - cmd.add("iptables"); - cmd.add("-t"); - cmd.add(table); - cmd.add("-I"); - cmd.add(chain); - cmd.add("1"); - cmd.addAll(Arrays.asList(args)); - return backend.iptables(cmd.toArray(new String[0])); + return rule("-I", "1", args); } @SuppressWarnings("unused") public boolean delete(@NonNull String... args) { - var cmd = new ArrayList(); - cmd.add("iptables"); - cmd.add("-t"); - cmd.add(table); - cmd.add("-D"); - cmd.add(chain); - cmd.addAll(Arrays.asList(args)); - return backend.iptables(cmd.toArray(new String[0])); + return rule("-D", null, args); } @SuppressWarnings("UnusedReturnValue") diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/iptables/IptablesBackend.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/iptables/IptablesBackend.java index 447c5f66f..ce4062773 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/iptables/IptablesBackend.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/iptables/IptablesBackend.java @@ -12,6 +12,7 @@ import cn.classfun.droidvm.daemon.network.NetworkInstance; import cn.classfun.droidvm.daemon.network.backend.FirewallHelper; +import cn.classfun.droidvm.lib.network.IPv6Network; import cn.classfun.droidvm.lib.utils.RunUtils; @SuppressWarnings("UnusedReturnValue") @@ -32,6 +33,11 @@ public final class IptablesBackend extends FirewallHelper { final ChainInfo NAT_PRE = new ChainInfo(this, "nat", "PREROUTING", "droidvm_pre"); final ChainInfo RAW_OUT = new ChainInfo(this, "raw", "OUTPUT", "droidvm_out"); final ChainInfo RAW_PRE = new ChainInfo(this, "raw", "PREROUTING", "droidvm_pre"); + final ChainInfo FIL6_NET_EXTNET_IN = new ChainInfo(this, true, "filter", null, "droidvm_ext_in"); + final ChainInfo FIL6_NET_EXTNET_OUT = new ChainInfo(this, true, "filter", null, "droidvm_ext_out"); + final ChainInfo FIL6_IN = new ChainInfo(this, true, "filter", "INPUT", "droidvm_in"); + final ChainInfo FIL6_OUT = new ChainInfo(this, true, "filter", "OUTPUT", "droidvm_out"); + final ChainInfo FIL6_FWD = new ChainInfo(this, true, "filter", "FORWARD", "droidvm_fwd"); final ChainInfo[] DEFS = new ChainInfo[]{ FIL_NET_EXTNET_IN, FIL_NET_EXTNET_OUT, @@ -48,7 +54,13 @@ public final class IptablesBackend extends FirewallHelper { NAT_PRE, RAW_OUT, RAW_PRE, + FIL6_NET_EXTNET_IN, + FIL6_NET_EXTNET_OUT, + FIL6_IN, + FIL6_OUT, + FIL6_FWD, }; + private static final String[] EXT_IFACES = {"wlan+", "eth+", "rmnet_data+"}; synchronized boolean iptables(@NonNull String... args) { int retry = 0; @@ -63,9 +75,9 @@ synchronized boolean iptables(@NonNull String... args) { } } - boolean iptablesCmd(@NonNull String table, @NonNull String... args) { + boolean iptablesCmd(@NonNull String binary, @NonNull String table, @NonNull String... args) { var cmd = new ArrayList(); - cmd.add("iptables"); + cmd.add(binary); cmd.add("-t"); cmd.add(table); cmd.addAll(Arrays.asList(args)); @@ -75,29 +87,30 @@ boolean iptablesCmd(@NonNull String table, @NonNull String... args) { @Override public boolean initialize() { Log.i(TAG, "Initializing iptables firewall chains"); - boolean ok = true; for (var def : DEFS) def.init(true); FIL_FWD.append("-m", "state", "--state", "RELATED,ESTABLISHED", "-j", "ACCEPT"); - FIL_NET_EXTNET_OUT.append("-o", "wlan+", "-j", "ACCEPT"); - FIL_NET_EXTNET_OUT.append("-o", "eth+", "-j", "ACCEPT"); - FIL_NET_EXTNET_OUT.append("-o", "rmnet_data+", "-j", "ACCEPT"); + FIL6_FWD.append("-m", "state", "--state", "RELATED,ESTABLISHED", "-j", "ACCEPT"); + for (var iface : EXT_IFACES) { + FIL_NET_EXTNET_OUT.append("-o", iface, "-j", "ACCEPT"); + FIL_NET_EXTNET_IN.append("-i", iface, "-j", "ACCEPT"); + FIL6_NET_EXTNET_OUT.append("-o", iface, "-j", "ACCEPT"); + FIL6_NET_EXTNET_IN.append("-i", iface, "-j", "ACCEPT"); + } FIL_NET_EXTNET_OUT.append("-j", "DROP"); - FIL_NET_EXTNET_IN.append("-i", "wlan+", "-j", "ACCEPT"); - FIL_NET_EXTNET_IN.append("-i", "eth+", "-j", "ACCEPT"); - FIL_NET_EXTNET_IN.append("-i", "rmnet_data+", "-j", "ACCEPT"); FIL_NET_EXTNET_IN.append("-j", "DROP"); + FIL6_NET_EXTNET_OUT.append("-j", "DROP"); + FIL6_NET_EXTNET_IN.append("-j", "DROP"); enableIpForward(); - return ok; + return true; } @Override public boolean shutdown() { Log.i(TAG, "Shutting down iptables firewall chains"); - boolean ok = true; for (var def : DEFS) def.deinit(); - return ok; + return true; } public void initNetwork(NetworkInstance inst) { @@ -112,39 +125,109 @@ public void deinitNetwork(NetworkInstance inst) { } @Override - public boolean applyForward( + public void addLiveV6Subnet( + @NonNull NetworkInstance inst, @NonNull String dev, @NonNull IPv6Network net + ) { + new IptablesNetworkInstance(this, inst).addLiveV6Subnet(dev, net); + } + + @Override + public void removeLiveV6Subnet( + @NonNull NetworkInstance inst, @NonNull String dev, @NonNull IPv6Network net + ) { + new IptablesNetworkInstance(this, inst).removeLiveV6Subnet(dev, net); + } + + @Override + public boolean applyForwardBase( @NonNull String bridge, @NonNull String guestIp, @NonNull String protocol, - @Nullable String hostIp, int hostPort, int guestPort + int guestStart, int guestEnd, @Nullable String hairpinSubnet ) { - var pre = new ChainInfo(this, "nat", null, fmt("droidvm_pre_%s", bridge)); var fwd = new ChainInfo(this, "filter", null, fmt("droidvm_fwd_%s", bridge)); - boolean ok = pre.append(dnatSpec(protocol, hostIp, hostPort, guestIp, guestPort)); - ok &= fwd.insert(fwdSpec(bridge, guestIp, protocol, guestPort)); - Log.i(TAG, fmt("Apply forward %s %s:%d -> %s:%d on %s (ok=%b)", - protocol, isSpecificHost(hostIp) ? hostIp : "*", - hostPort, guestIp, guestPort, bridge, ok)); + boolean ok = fwd.insert(fwdSpec(bridge, guestIp, protocol, guestStart, guestEnd)); + if (hairpinSubnet != null) { + var pst = new ChainInfo(this, "nat", null, fmt("droidvm_pst_%s", bridge)); + ok &= pst.append(hairpinSpec( + protocol, hairpinSubnet, guestIp, guestStart, guestEnd)); + } + Log.i(TAG, fmt("Apply forward base %s -> %s:%s on %s (ok=%b)", + protocol, guestIp, portRange(guestStart, guestEnd, ':'), bridge, ok)); return ok; } @Override - public boolean removeForward( + public boolean removeForwardBase( @NonNull String bridge, @NonNull String guestIp, @NonNull String protocol, - @Nullable String hostIp, int hostPort, int guestPort + int guestStart, int guestEnd, @Nullable String hairpinSubnet ) { - var pre = new ChainInfo(this, "nat", null, fmt("droidvm_pre_%s", bridge)); var fwd = new ChainInfo(this, "filter", null, fmt("droidvm_fwd_%s", bridge)); - boolean ok = pre.delete(dnatSpec(protocol, hostIp, hostPort, guestIp, guestPort)); - ok &= fwd.delete(fwdSpec(bridge, guestIp, protocol, guestPort)); - Log.i(TAG, fmt("Remove forward %s %s:%d -> %s:%d on %s (ok=%b)", - protocol, isSpecificHost(hostIp) ? hostIp : "*", - hostPort, guestIp, guestPort, bridge, ok)); + boolean ok = fwd.delete(fwdSpec(bridge, guestIp, protocol, guestStart, guestEnd)); + if (hairpinSubnet != null) { + var pst = new ChainInfo(this, "nat", null, fmt("droidvm_pst_%s", bridge)); + ok &= pst.delete(hairpinSpec( + protocol, hairpinSubnet, guestIp, guestStart, guestEnd)); + } + Log.i(TAG, fmt("Remove forward base %s -> %s:%s on %s (ok=%b)", + protocol, guestIp, portRange(guestStart, guestEnd, ':'), bridge, ok)); + return ok; + } + + @Override + public boolean applyDnat( + @NonNull String bridge, @NonNull String guestIp, @NonNull String protocol, + @NonNull String hostIp, int hostStart, int hostEnd, int guestStart, int guestEnd + ) { + var pre = new ChainInfo(this, "nat", null, fmt("droidvm_pre_%s", bridge)); + boolean ok = pre.append( + dnatSpec(protocol, hostIp, hostStart, hostEnd, guestIp, guestStart, guestEnd)); + Log.i(TAG, fmt("Apply DNAT %s %s:%s -> %s:%s on %s (ok=%b)", + protocol, hostIp, portRange(hostStart, hostEnd, ':'), + guestIp, portRange(guestStart, guestEnd, ':'), bridge, ok)); + return ok; + } + + @Override + public boolean removeDnat( + @NonNull String bridge, @NonNull String guestIp, @NonNull String protocol, + @NonNull String hostIp, int hostStart, int hostEnd, int guestStart, int guestEnd + ) { + var pre = new ChainInfo(this, "nat", null, fmt("droidvm_pre_%s", bridge)); + boolean ok = pre.delete( + dnatSpec(protocol, hostIp, hostStart, hostEnd, guestIp, guestStart, guestEnd)); + Log.i(TAG, fmt("Remove DNAT %s %s:%s -> %s:%s on %s (ok=%b)", + protocol, hostIp, portRange(hostStart, hostEnd, ':'), + guestIp, portRange(guestStart, guestEnd, ':'), bridge, ok)); return ok; } + /** + * NAT loopback: a guest in hairpinSubnet hitting the forward's guest + * target gets its source masqueraded to the gateway, so the target's + * reply routes back through the gateway instead of straight over L2 + * (where the originator would see an unexpected source). + */ + @NonNull + private static String[] hairpinSpec( + @NonNull String protocol, @NonNull String hairpinSubnet, + @NonNull String guestIp, int guestStart, int guestEnd + ) { + return new String[]{ + "-s", hairpinSubnet, "-d", guestIp, "-p", protocol, + "--dport", portRange(guestStart, guestEnd, ':'), "-j", "MASQUERADE" + }; + } + + @NonNull + private static String portRange(int start, int end, char sep) { + return start == end + ? String.valueOf(start) + : fmt("%d%c%d", start, sep, end); + } + @NonNull private static String[] dnatSpec( - @NonNull String protocol, @Nullable String hostIp, int hostPort, - @NonNull String guestIp, int guestPort + @NonNull String protocol, @Nullable String hostIp, int hostStart, int hostEnd, + @NonNull String guestIp, int guestStart, int guestEnd ) { var spec = new ArrayList(); spec.add("-p"); @@ -154,22 +237,23 @@ private static String[] dnatSpec( spec.add(hostIp); } spec.add("--dport"); - spec.add(String.valueOf(hostPort)); + spec.add(portRange(hostStart, hostEnd, ':')); spec.add("-j"); spec.add("DNAT"); spec.add("--to-destination"); - spec.add(fmt("%s:%d", guestIp, guestPort)); + // iptables DNAT port ranges use a dash: ip:8000-9000 + spec.add(fmt("%s:%s", guestIp, portRange(guestStart, guestEnd, '-'))); return spec.toArray(new String[0]); } @NonNull private static String[] fwdSpec( @NonNull String bridge, @NonNull String guestIp, - @NonNull String protocol, int guestPort + @NonNull String protocol, int guestStart, int guestEnd ) { return new String[]{ - "-o", bridge, "-d", guestIp, "-p", protocol, - "--dport", String.valueOf(guestPort), "-j", "ACCEPT" + "-o", fmt("%s+", bridge), "-d", guestIp, "-p", protocol, + "--dport", portRange(guestStart, guestEnd, ':'), "-j", "ACCEPT" }; } @@ -181,8 +265,9 @@ private static boolean isSpecificHost(@Nullable String hostIp) { } private boolean enableIpForward() { - Log.i(TAG, "Enabling IPv4 forwarding"); + Log.i(TAG, "Enabling IP forwarding"); var result = RunUtils.run("echo 1 > /proc/sys/net/ipv4/ip_forward"); + RunUtils.run("echo 1 > /proc/sys/net/ipv6/conf/all/forwarding"); return result.isSuccess(); } } diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/iptables/IptablesNetworkInstance.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/iptables/IptablesNetworkInstance.java index 8c95dbb54..a8224c476 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/iptables/IptablesNetworkInstance.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/iptables/IptablesNetworkInstance.java @@ -5,9 +5,16 @@ import androidx.annotation.NonNull; import cn.classfun.droidvm.daemon.network.NetworkInstance; +import cn.classfun.droidvm.daemon.network.backend.LinuxNetwork; import cn.classfun.droidvm.lib.network.IPv4Network; -import cn.classfun.droidvm.lib.store.base.DataItem; +import cn.classfun.droidvm.lib.network.IPv6Network; +import cn.classfun.droidvm.lib.store.network.VlanConfig; +/** + * Per-network iptables/ip6tables chains for an L3-routed Linux bridge: + * per-VLAN subnet accept rules, MASQUERADE for SNAT-enabled VLANs and the + * per-bridge chains that hold DNAT port forwards. + */ final class IptablesNetworkInstance { final IptablesBackend backend; final NetworkInstance inst; @@ -24,6 +31,9 @@ final class IptablesNetworkInstance { final ChainInfo magPst; final ChainInfo rawOut; final ChainInfo rawPre; + final ChainInfo filIn6; + final ChainInfo filOut6; + final ChainInfo filFwd6; IptablesNetworkInstance(IptablesBackend backend, @NonNull NetworkInstance inst) { this.backend = backend; @@ -41,6 +51,9 @@ final class IptablesNetworkInstance { magPst = new ChainInfo(backend, backend.MAG_PST, fmt("droidvm_pst_%s", br)); rawOut = new ChainInfo(backend, backend.RAW_OUT, fmt("droidvm_out_%s", br)); rawPre = new ChainInfo(backend, backend.RAW_PRE, fmt("droidvm_pre_%s", br)); + filIn6 = new ChainInfo(backend, backend.FIL6_IN, fmt("droidvm_in_%s", br)); + filOut6 = new ChainInfo(backend, backend.FIL6_OUT, fmt("droidvm_out_%s", br)); + filFwd6 = new ChainInfo(backend, backend.FIL6_FWD, fmt("droidvm_fwd_%s", br)); } void init() { @@ -56,6 +69,9 @@ void init() { magPst.init(false); rawOut.init(false); rawPre.init(false); + filIn6.init(false); + filOut6.init(false); + filFwd6.init(false); } void deinit() { @@ -71,30 +87,107 @@ void deinit() { magPst.deinit(); rawOut.deinit(); rawPre.deinit(); + filIn6.deinit(); + filOut6.deinit(); + filFwd6.deinit(); + } + + @NonNull + private String deviceOf(@NonNull VlanConfig vlan) { + return LinuxNetwork.vlanDevice(br, vlan.getVlanId()); } void initBasic() { - boolean canIn = inst.item.optBoolean("nat", false) || inst.item.optBoolean("access", false); - boolean canOut = inst.item.optBoolean("access", false); var extnetIn = backend.FIL_NET_EXTNET_IN.chain; var extnetOut = backend.FIL_NET_EXTNET_OUT.chain; + var extnetIn6 = backend.FIL6_NET_EXTNET_IN.chain; + var extnetOut6 = backend.FIL6_NET_EXTNET_OUT.chain; var input = backend.FIL_NET_INPUT.chain; - filFwd.addChain(br, br, null, null, "ACCEPT", null); - filOut.addChain(null, br, null, null, "ACCEPT", null); - for (var iter : inst.item.opt("ipv4_addresses", DataItem.newArray())) { - IPv4Network addr = IPv4Network.parse(iter.getValue().asString()); - String router = new IPv4Network(addr.address(), 32).toString(); - String network = addr.network().toString(); - filIn.addChain(br, null, network, router, input, null); - filFwd.addChain(br, null, network, null, canIn ? extnetOut : "DROP", null); - filFwd.addChain(null, br, null, network, canOut ? extnetIn : "DROP", null); - if (inst.item.optBoolean("nat", false)) { - natPst.addChain(br, null, network, null, "MASQUERADE", null); - natPst.addChain(null, br, null, network, "MASQUERADE", null); + // bridged + inter-VLAN routed traffic within this network + var wildcard = fmt("%s+", br); + filFwd.addChain(wildcard, wildcard, null, null, "ACCEPT", null); + filFwd6.addChain(wildcard, wildcard, null, null, "ACCEPT", null); + filOut.addChain(null, wildcard, null, null, "ACCEPT", null); + filOut6.addChain(null, wildcard, null, null, "ACCEPT", null); + for (var vlan : inst.getVlans()) { + var dev = deviceOf(vlan); + boolean snat4 = vlan.isIpv4Snat(); + var net4 = vlan.getIpv4Network(); + if (net4 != null) + addV4Subnet(dev, net4, snat4, input, extnetIn, extnetOut); + for (var cidr : vlan.getIpv4Secondary()) { + try { + addV4Subnet(dev, IPv4Network.parse(cidr), + snat4, input, extnetIn, extnetOut); + } catch (Exception ignored) { + } + } + var net6 = vlan.getIpv6Network(); + if (net6 != null) addV6Subnet(dev, net6, extnetIn6, extnetOut6); + for (var cidr : vlan.getIpv6Secondary()) { + try { + addV6Subnet(dev, IPv6Network.parse(cidr), extnetIn6, extnetOut6); + } catch (Exception ignored) { + } } } - magPre.addChain(br, null, null, null, "ACCEPT", null); - filFwd.addChain(br, null, null, null, "DROP", null); - filFwd.addChain(null, br, null, null, "DROP", null); + magPre.addChain(wildcard, null, null, null, "ACCEPT", null); + filFwd.addChain(wildcard, null, null, null, "DROP", null); + filFwd.addChain(null, wildcard, null, null, "DROP", null); + filFwd6.addChain(wildcard, null, null, null, "DROP", null); + filFwd6.addChain(null, wildcard, null, null, "DROP", null); + } + + private void addV4Subnet( + @NonNull String dev, @NonNull IPv4Network addr, boolean snat, + String input, String extnetIn, String extnetOut + ) { + var router = new IPv4Network(addr.address(), 32).toString(); + var network = addr.toNetworkString(); + filIn.addChain(dev, null, network, router, input, null); + filFwd.addChain(dev, null, network, null, extnetOut, null); + filFwd.addChain(null, dev, null, network, extnetIn, null); + if (snat) { + // SNAT guest -> external only. POSTROUTING has no input + // interface (`-i` is invalid there), so match on the source + // subnet leaving via a non-bridge interface; `! -o {br}+` + // excludes inter-VLAN traffic, which keeps its real source. + natPst.append("-s", network, "!", "-o", fmt("%s+", br), "-j", "MASQUERADE"); + } + } + + private void addV6Subnet( + @NonNull String dev, @NonNull IPv6Network addr, + String extnetIn6, String extnetOut6 + ) { + var network = addr.toNetworkString(); + var router = fmt("%s/128", addr.address()); + filIn6.addChain(dev, null, network, router, "ACCEPT", null); + filFwd6.addChain(dev, null, network, null, extnetOut6, null); + filFwd6.addChain(null, dev, null, network, extnetIn6, null); + } + + /** + * Adds rules for a delegated prefix acquired after network start; + * inserted at the top so they precede the trailing DROP rules. + */ + void addLiveV6Subnet(@NonNull String dev, @NonNull IPv6Network addr) { + var network = addr.toNetworkString(); + var router = fmt("%s/128", addr.address()); + filIn6.insert("-i", dev, "-s", network, "-d", router, "-j", "ACCEPT"); + filFwd6.insert("-i", dev, "-s", network, + "-j", backend.FIL6_NET_EXTNET_OUT.chain); + filFwd6.insert("-o", dev, "-d", network, + "-j", backend.FIL6_NET_EXTNET_IN.chain); + } + + void removeLiveV6Subnet(@NonNull String dev, @NonNull IPv6Network addr) { + var network = addr.toNetworkString(); + var router = fmt("%s/128", addr.address()); + filIn6.delete("-i", dev, "-s", network, "-d", router, "-j", "ACCEPT"); + filFwd6.delete("-i", dev, "-s", network, + "-j", backend.FIL6_NET_EXTNET_OUT.chain); + filFwd6.delete("-o", dev, "-d", network, + "-j", backend.FIL6_NET_EXTNET_IN.chain); } } diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/pd/Duid.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/pd/Duid.java new file mode 100644 index 000000000..844e0ba29 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/pd/Duid.java @@ -0,0 +1,50 @@ +package cn.classfun.droidvm.daemon.network.backend.pd; + +import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * DHCPv6 DUID helpers. The default is DUID-LL (type 3, hw type 1) + * derived from a MAC address; user-supplied DUIDs are colon-hex strings. + */ +public final class Duid { + private Duid() { + } + + @NonNull + public static byte[] fromLinkLayer(@NonNull String mac) { + var parts = mac.split(":"); + if (parts.length != 6) + throw new IllegalArgumentException(fmt("Invalid MAC for DUID-LL: %s", mac)); + var out = new byte[4 + 6]; + out[0] = 0x00; + out[1] = 0x03; // DUID-LL + out[2] = 0x00; + out[3] = 0x01; // hw type: ethernet + for (int i = 0; i < 6; i++) + out[4 + i] = (byte) Integer.parseInt(parts[i], 16); + return out; + } + + @NonNull + public static byte[] parse(@NonNull String hex) { + var parts = hex.split(":"); + var out = new byte[parts.length]; + for (int i = 0; i < parts.length; i++) + out[i] = (byte) Integer.parseInt(parts[i], 16); + return out; + } + + @NonNull + public static String format(@Nullable byte[] duid) { + if (duid == null) return ""; + var sb = new StringBuilder(); + for (var b : duid) { + if (sb.length() > 0) sb.append(':'); + sb.append(fmt("%02x", b)); + } + return sb.toString(); + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/server/ServerContext.java b/app/src/main/java/cn/classfun/droidvm/daemon/server/ServerContext.java index 585309f3b..928ab3a9d 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/server/ServerContext.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/server/ServerContext.java @@ -28,7 +28,10 @@ public ServerContext() { networks.load(new File(filesDir, networks.getFileName())); Log.i(TAG, fmt("config files loaded: %d VMs, %d networks", vms.size(), networks.size())); vms.setNetworkStore(networks); + // strays survive a daemon crash (children are orphaned, not killed) run("pkill dnsmasq"); + run("pkill gvswitch"); + run("pkill pbridge"); try { networks.firewall.initialize(); } catch (Exception e) { diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMInstance.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMInstance.java index d8750b0fd..8bb270bec 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMInstance.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMInstance.java @@ -22,7 +22,6 @@ import java.util.UUID; import cn.classfun.droidvm.daemon.console.ConsoleStream; -import cn.classfun.droidvm.daemon.network.backend.LinuxNetwork; import cn.classfun.droidvm.daemon.vm.backend.BackendBase; import cn.classfun.droidvm.lib.natives.NativeProcess; import cn.classfun.droidvm.lib.natives.UnixHelper; @@ -30,6 +29,7 @@ import cn.classfun.droidvm.lib.store.network.NetworkState; import cn.classfun.droidvm.lib.store.vm.VMBackend; import cn.classfun.droidvm.lib.store.vm.VMConfig; +import cn.classfun.droidvm.lib.store.vm.VMNicConfig; import cn.classfun.droidvm.lib.store.vm.VMState; import cn.classfun.droidvm.lib.utils.JsonUtils; @@ -42,7 +42,6 @@ public final class VMInstance extends VMConfig { private BackendBase backend; private VMBackendInstance backendInstance; private Thread workerThread; - private volatile VMPortForwarder portForwarder; private final VMInstanceStore store; public interface VMEventCallback { @@ -145,12 +144,12 @@ public boolean start() { return true; } - private void setupTap(int index, List createdTaps, @NonNull DataItem netCfg, String vmId) { - var netId = netCfg.optString("network_id", ""); - if (netId == null || netId.isEmpty()) return; + private void setupTap(int index, List createdNics, @NonNull DataItem netCfg, String vmId) throws Exception { + var nic = new VMNicConfig(netCfg); + var netId = nic.getNetworkId(); + if (netId == null) return; var netInst = store.networkStore.findById(UUID.fromString(netId)); if (netInst == null) throw new RuntimeException(fmt("Network %s not found", netId)); - var br = netInst.item.optString("bridge_name", ""); var netState = netInst.getState(); if (netState != NetworkState.RUNNING) { Log.i(TAG, fmt("Network %s is %s, attempting to start it", netId, netState)); @@ -161,24 +160,16 @@ private void setupTap(int index, List createdTaps, @NonNull DataItem net throw new RuntimeException(fmt("Network %s still not running after start (state=%s)", netId, netState)); Log.i(TAG, fmt("Network %s started successfully", netId)); } - var tapName = requireNonNull(netCfg.optString("tap_name", "")); - if (tapName.isEmpty()) { + var tapName = nic.getTapName(); + if (tapName == null) { tapName = fmt("vm%s-%d", vmId.substring(0, 8), index); - netCfg.set("tap_name", tapName); + nic.setTapName(tapName); } - var bridge = store.networkStore.backend; - if (bridge.isInterfaceExists(tapName)) bridge.deleteTap(tapName); - if (!bridge.createTap(tapName)) - throw new RuntimeException(fmt("Failed to create TAP %s", tapName)); - createdTaps.add(tapName); - if (!netInst.addInterface(tapName)) - throw new RuntimeException(fmt("Failed to add TAP %s to bridge %s", tapName, br)); - var tapMac = requireNonNull(netCfg.optString("mac_address", "")); - if (!tapMac.isEmpty() && !bridge.setMacAddress(tapName, tapMac)) - Log.w(TAG, fmt("Failed to set MAC %s on TAP %s", tapMac, tapName)); - if (!bridge.setLinkState(tapName, true)) - Log.w(TAG, fmt("Failed to bring up TAP %s", tapName)); - Log.i(TAG, fmt("TAP %s attached to bridge %s for VM %s", tapName, br, vmId)); + netInst.attachNic(nic, tapName); + createdNics.add(netCfg); + Log.i(TAG, fmt( + "NIC %s attached to network %s for VM %s", tapName, netInst.getName(), vmId + )); } private boolean setupTaps() { @@ -189,25 +180,38 @@ private boolean setupTaps() { return false; } var vmId = getId().toString(); - var createdTaps = new ArrayList(); + var createdNics = new ArrayList(); try { var arr = nets.asArray(); for (int i = 0; i < arr.size(); i++) { - setupTap(i, createdTaps, arr.get(i), vmId); + setupTap(i, createdNics, arr.get(i), vmId); } } catch (Exception e) { Log.e(TAG, fmt("Error setting up TAP for VM %s: %s", vmId, e.getMessage()), e); - cleanupCreatedTaps(createdTaps); + cleanupCreatedNics(createdNics); return false; } return true; } - private void cleanupCreatedTaps(@NonNull List taps) { - var net = store.networkStore.backend; - for (var tap : taps) { - net.removeInterface(tap); - net.deleteTap(tap); + private void cleanupCreatedNics(@NonNull List nics) { + for (var netCfg : nics) + detachNic(new VMNicConfig(netCfg)); + } + + private void detachNic(@NonNull VMNicConfig nic) { + var tapName = nic.getTapName(); + if (tapName == null) return; + var netId = nic.getNetworkId(); + var netInst = netId != null + ? store.networkStore.findById(UUID.fromString(netId)) : null; + if (netInst != null) { + netInst.detachNic(nic, tapName); + } else { + // network gone; remove the tap directly + var net = store.networkStore.backend; + net.removeInterface(tapName); + net.deleteTap(tapName); } } @@ -327,7 +331,6 @@ private void runVM() { startReaderThread(vmId, stream); } setState(VMState.RUNNING); - startPortForwarding(); int code = process.waitFor(); for (var stream : inst.streams.values()) { var reader = stream.getReaderThread(); @@ -352,7 +355,6 @@ private void runVM() { } else { Log.i(TAG, fmt("VM %s exited with code %d", getName(), code)); } - stopPortForwarding(); cleanupTap(); inst.cleanup(); setState(VMState.STOPPED); @@ -362,58 +364,12 @@ private void runVM() { private void cleanupTap() { var nets = requireNonNull(item.opt("networks", DataItem.newArray())); if (nets.isEmpty()) return; - var bridge = new LinuxNetwork(); for (var iter : nets) { - var tapVal = iter.getValue().optString("tap_name", ""); - if (tapVal == null || tapVal.isEmpty()) continue; - Log.i(TAG, fmt("Cleaning up TAP %s for VM %s", tapVal, getName())); - bridge.removeInterface(tapVal); - bridge.deleteTap(tapVal); - } - } - - private void startPortForwarding() { - try { - portForwarder = new VMPortForwarder(this, store.networkStore); - portForwarder.start(); - } catch (Exception e) { - Log.w(TAG, fmt("Failed to start port forwarding for VM %s", getName()), e); - } - } - - private void stopPortForwarding() { - if (portForwarder == null) return; - try { - portForwarder.stop(); - } catch (Exception e) { - Log.w(TAG, fmt("Failed to stop port forwarding for VM %s", getName()), e); + var nic = new VMNicConfig(iter.getValue()); + if (nic.getTapName() == null) continue; + Log.i(TAG, fmt("Cleaning up TAP %s for VM %s", nic.getTapName(), getName())); + detachNic(nic); } - portForwarder = null; - } - - @NonNull - public JSONArray getConfiguredPortForwards() throws JSONException { - var arr = new JSONArray(); - var pf = item.opt("port_forwards", null); - if (pf != null && pf.is(DataItem.Type.ARRAY)) - for (var iter : pf) { - var r = iter.getValue(); - if (r.is(DataItem.Type.OBJECT)) arr.put(r.toJson()); - } - return arr; - } - - @NonNull - public JSONArray getActivePortForwards() { - var pf = portForwarder; - return pf != null ? pf.snapshotApplied() : new JSONArray(); - } - - /** Runtime-only hot-update of DNAT rules; the frontend owns persistence (re-pushed via vm_modify on next start). */ - public void applyPortForwards(@NonNull JSONArray rules) { - item.set("port_forwards", rules); - var pf = portForwarder; - if (pf != null) pf.sync(); } private void startReaderThread(@NonNull String vmId, @NonNull ConsoleStream stream) { diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMPortForwarder.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMPortForwarder.java deleted file mode 100644 index 1f675ec4f..000000000 --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMPortForwarder.java +++ /dev/null @@ -1,359 +0,0 @@ -package cn.classfun.droidvm.daemon.vm; - -import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; - -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; - -import cn.classfun.droidvm.daemon.network.NetworkInstance; -import cn.classfun.droidvm.daemon.network.NetworkInstanceStore; -import cn.classfun.droidvm.lib.store.base.DataItem; -import cn.classfun.droidvm.lib.store.vm.VMState; - -/** - * Installs host -> guest iptables DNAT rules for one VM's {@code port_forwards}, resolving the - * guest IP from DHCP leases / the neighbor table while the VM is RUNNING. - */ -final class VMPortForwarder { - private static final String TAG = "VMPortForwarder"; - private static final int RESOLVE_MAX_ATTEMPTS = 30; - private static final long RESOLVE_INTERVAL_MS = 1000L; - - private final VMInstance vm; - private final NetworkInstanceStore networkStore; - private final List applied = new ArrayList<>(); - private volatile boolean running = false; - private Thread thread; - - VMPortForwarder(@NonNull VMInstance vm, @Nullable NetworkInstanceStore networkStore) { - this.vm = vm; - this.networkStore = networkStore; - } - - boolean hasRules() { - var pf = vm.item.opt("port_forwards", null); - return pf != null && !pf.isEmpty(); - } - - synchronized void start() { - if (running) return; - if (!hasRules()) return; - if (networkStore == null) { - Log.w(TAG, fmt("VM %s has port forwards but no network store", vm.getName())); - return; - } - running = true; - thread = new Thread(this::loop, fmt("PF-%s", vm.getId())); - thread.setDaemon(true); - thread.start(); - } - - synchronized void stop() { - running = false; - notifyAll(); - var t = thread; - if (t != null) { - t.interrupt(); - thread = null; - } - removeAll(); - } - - @NonNull - JSONArray snapshotApplied() { - var arr = new JSONArray(); - synchronized (applied) { - for (var a : applied) { - try { - var o = new JSONObject(); - o.put("protocol", a.protocol); - o.put("host_ip", a.hostIp == null ? "" : a.hostIp); - o.put("host_port", a.hostPort); - o.put("guest_ip", a.guestIp); - o.put("guest_port", a.guestPort); - arr.put(o); - } catch (JSONException ignored) { - } - } - } - return arr; - } - - /** Wakes the poll thread (or starts it) so reconcile always runs on that single thread. */ - synchronized void sync() { - if (running) notifyAll(); - else start(); - } - - /** - * Incrementally reconciles installed rules to the current config. Poll-thread only and the - * sole writer of {@link #applied} (besides {@link #stop()}). A still-configured binding whose - * guest IP is momentarily unresolvable keeps its old entry and is switched atomically once - * resolvable, so hot-editing guest_ip/NIC never drops a working forward. - * - * @return whether any rule is still configured-but-unresolvable (keep polling). - */ - private boolean reconcile() { - if (!running || networkStore == null) return false; - var rules = parseRules(); - // Resolve outside the applied lock; DHCP / neighbor lookups are slow. - var desired = new ArrayList(); - boolean hasUnresolved = false; - for (var rule : rules) { - var target = resolveTarget(rule); - if (target == null) { - hasUnresolved = true; - continue; - } - desired.add(new Applied(target.bridge, target.ip, rule.protocol, - rule.hostIp, rule.hostPort, rule.guestPort)); - } - synchronized (applied) { - applied.removeIf(a -> { - for (var d : desired) - if (sameForward(a, d)) return false; - if (hostBindingConfigured(a, rules)) return false; - networkStore.firewall.removeForward( - a.bridge, a.guestIp, a.protocol, a.hostIp, a.hostPort, a.guestPort); - Log.i(TAG, fmt("VM %s: removed forward %s :%d -> %s:%d", - vm.getName(), a.protocol, a.hostPort, a.guestIp, a.guestPort)); - return true; - }); - for (var d : desired) { - boolean exists = false; - for (var a : applied) - if (sameForward(a, d)) { - exists = true; - break; - } - if (exists) continue; - applied.removeIf(a -> { - if (!sameHostBinding(a, d)) return false; - networkStore.firewall.removeForward( - a.bridge, a.guestIp, a.protocol, a.hostIp, a.hostPort, a.guestPort); - Log.i(TAG, fmt("VM %s: switched forward %s :%d off %s:%d", - vm.getName(), a.protocol, a.hostPort, a.guestIp, a.guestPort)); - return true; - }); - boolean ok = networkStore.firewall.applyForward( - d.bridge, d.guestIp, d.protocol, d.hostIp, d.hostPort, d.guestPort); - if (ok) { - applied.add(d); - Log.i(TAG, fmt("VM %s: forward %s :%d -> %s:%d", - vm.getName(), d.protocol, d.hostPort, d.guestIp, d.guestPort)); - } else { - Log.w(TAG, fmt("VM %s: failed to apply forward %s :%d", - vm.getName(), d.protocol, d.hostPort)); - } - } - } - return hasUnresolved; - } - - private static boolean sameForward(@NonNull Applied a, @NonNull Applied b) { - return a.protocol.equals(b.protocol) - && a.hostPort == b.hostPort - && a.guestPort == b.guestPort - && eq(a.hostIp, b.hostIp) - && eq(a.guestIp, b.guestIp) - && eq(a.bridge, b.bridge); - } - - private static boolean sameHostBinding(@NonNull Applied a, @NonNull Applied b) { - return a.protocol.equals(b.protocol) && a.hostPort == b.hostPort && eq(a.hostIp, b.hostIp); - } - - private static boolean hostBindingConfigured(@NonNull Applied a, @NonNull List rules) { - for (var r : rules) - if (a.protocol.equals(r.protocol) && a.hostPort == r.hostPort && eq(a.hostIp, r.hostIp)) - return true; - return false; - } - - private static boolean eq(@Nullable String a, @Nullable String b) { - return a == null ? b == null : a.equals(b); - } - - /** Reconcile/wait loop; stays alive the whole RUNNING period so late rule changes are still reconciled. */ - private void loop() { - try { - int attempts = 0; - synchronized (this) { - while (running && vm.getState() == VMState.RUNNING) { - boolean hasUnresolved = reconcile(); - if (!running) break; - if (hasUnresolved && attempts < RESOLVE_MAX_ATTEMPTS) { - attempts++; - wait(RESOLVE_INTERVAL_MS); - } else { - if (hasUnresolved) - Log.w(TAG, fmt("VM %s: gave up auto-resolving guest IP for some port" - + " forwards after %d attempts; will retry on next config change", - vm.getName(), attempts)); - attempts = 0; - wait(); - } - } - } - } catch (InterruptedException ignored) { - Thread.currentThread().interrupt(); - } - } - - private void removeAll() { - synchronized (applied) { - if (networkStore != null) - for (var a : applied) - networkStore.firewall.removeForward( - a.bridge, a.guestIp, a.protocol, a.hostIp, a.hostPort, a.guestPort); - applied.clear(); - } - } - - @NonNull - private List parseRules() { - var list = new ArrayList(); - var pf = vm.item.opt("port_forwards", null); - if (pf == null || !pf.is(DataItem.Type.ARRAY)) return list; - var seen = new HashSet(); - for (var iter : pf) { - var r = iter.getValue(); - if (!r.is(DataItem.Type.OBJECT)) continue; - if (!r.optBoolean("enabled", true)) continue; - var protocol = r.optString("protocol", "tcp"); - if (protocol == null || protocol.isEmpty()) protocol = "tcp"; - protocol = protocol.toLowerCase(Locale.ROOT); - if (!protocol.equals("tcp") && !protocol.equals("udp")) continue; - long hostPort = r.optLong("host_port", 0); - if (hostPort <= 0 || hostPort > 65535) continue; - long guestPort = r.optLong("guest_port", 0); - if (guestPort <= 0 || guestPort > 65535) guestPort = hostPort; - var hostIp = r.optString("host_ip", ""); - if (hostIp == null) hostIp = ""; - var key = fmt("%s|%s|%d", protocol, hostIp, hostPort); - if (!seen.add(key)) { - Log.w(TAG, fmt("VM %s: duplicate port forward %s, skipping", vm.getName(), key)); - continue; - } - var rule = new Rule(); - rule.protocol = protocol; - rule.hostIp = hostIp; - rule.hostPort = (int) hostPort; - rule.guestPort = (int) guestPort; - rule.networkId = r.optString("network_id", ""); - rule.fixedGuestIp = r.optString("guest_ip", ""); - list.add(rule); - } - return list; - } - - @Nullable - private Target resolveTarget(@NonNull Rule rule) { - var net = findNetwork(rule.networkId); - if (net == null) return null; - var netId = net.optString("network_id", ""); - if (netId == null || netId.isEmpty()) return null; - var netInst = networkStore.findById(netId); - if (netInst == null) return null; - var bridge = netInst.item.optString("bridge_name", ""); - if (bridge == null || bridge.isEmpty()) return null; - if (rule.fixedGuestIp != null && !rule.fixedGuestIp.isEmpty()) - return new Target(bridge, rule.fixedGuestIp); - var mac = net.optString("mac_address", ""); - if (mac == null || mac.isEmpty()) return null; - var ip = resolveGuestIpByMac(netInst, bridge, mac); - if (ip == null) return null; - return new Target(bridge, ip); - } - - @Nullable - private String resolveGuestIpByMac( - @NonNull NetworkInstance netInst, @NonNull String bridge, @NonNull String mac) { - var macLower = mac.toLowerCase(Locale.ROOT); - var ip = matchMac(networkStore.backend.listDhcpLeases(bridge), macLower, "ip", "mac"); - if (ip != null) return ip; - return matchMac(netInst.listNeighbors(), macLower, "dst", "lladdr"); - } - - @Nullable - private static String matchMac( - @Nullable JSONArray arr, @NonNull String macLower, - @NonNull String ipKey, @NonNull String macKey) { - if (arr == null) return null; - for (int i = 0; i < arr.length(); i++) { - var o = arr.optJSONObject(i); - if (o == null) continue; - var m = o.optString(macKey, ""); - if (!m.isEmpty() && m.toLowerCase(Locale.ROOT).equals(macLower)) { - var ip = o.optString(ipKey, ""); - if (!ip.isEmpty()) return ip; - } - } - return null; - } - - @Nullable - private DataItem findNetwork(@Nullable String networkId) { - var nets = vm.item.opt("networks", null); - if (nets == null || !nets.is(DataItem.Type.ARRAY)) return null; - DataItem first = null; - for (var iter : nets) { - var net = iter.getValue(); - var nid = net.optString("network_id", ""); - if (nid == null || nid.isEmpty()) continue; - if (first == null) first = net; - if (networkId == null || networkId.isEmpty()) return net; - if (networkId.equals(nid)) return net; - } - return first; - } - - private static final class Applied { - final String bridge; - final String guestIp; - final String protocol; - final String hostIp; - final int hostPort; - final int guestPort; - - Applied(String bridge, String guestIp, String protocol, - String hostIp, int hostPort, int guestPort) { - this.bridge = bridge; - this.guestIp = guestIp; - this.protocol = protocol; - this.hostIp = hostIp; - this.hostPort = hostPort; - this.guestPort = guestPort; - } - } - - private static final class Rule { - String protocol; - String hostIp; - int hostPort; - int guestPort; - String networkId; - String fixedGuestIp; - } - - private static final class Target { - final String bridge; - final String ip; - - Target(String bridge, String ip) { - this.bridge = bridge; - this.ip = ip; - } - } -} diff --git a/app/src/main/java/cn/classfun/droidvm/lib/Constants.java b/app/src/main/java/cn/classfun/droidvm/lib/Constants.java index 0df2d8ed4..4670a715e 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/Constants.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/Constants.java @@ -23,6 +23,13 @@ public final class Constants { public static final String[] BINARIES_PREBUILT = {"7za"}; public static final String[] BINARIES_BUILT = {"droidvm", "daemon"}; public static final String[] LIBRARIES_BUILT = {"libsimpledump.so", "libunixhelper.so"}; + /** + * IFA_RT_PRIORITY magic that pbridge tags its ND/ARP-offload proxy + * addresses with (its --offload-workaround-magic). Host-IP discovery + * excludes addresses carrying this metric: they are guest addresses + * pbridge parks on the uplink, not the phone's own addresses. + */ + public static final long PBRIDGE_OFFLOAD_MAGIC = 4243672773L; private Constants() { } diff --git a/app/src/main/java/cn/classfun/droidvm/lib/network/IPv4Network.java b/app/src/main/java/cn/classfun/droidvm/lib/network/IPv4Network.java index 4bd7ef0ee..e293d2663 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/network/IPv4Network.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/network/IPv4Network.java @@ -95,6 +95,19 @@ public boolean overlaps(@NonNull IPv4Network other) { return thisNet <= otherBcast && otherNet <= thisBcast; } + /** + * Address at network base + offset. Offset must address a usable host + * within this prefix (not the network or broadcast address). + */ + @NonNull + public IPv4Address addressAtOffset(long offset) { + long total = totalAddresses(); + if (offset <= 0 || offset >= total - 1) throw new IllegalArgumentException(fmt( + "Offset %d out of range for /%d network (1..%d)", offset, prefix, total - 2 + )); + return networkAddress().add(offset); + } + @Nullable public IPv4Address dhcpPoolStart() { if (prefix >= 31) return null; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/network/IPv6Network.java b/app/src/main/java/cn/classfun/droidvm/lib/network/IPv6Network.java index 488041fe9..34b0918f7 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/network/IPv6Network.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/network/IPv6Network.java @@ -81,6 +81,24 @@ public IPv6Address lastAddress() { return new IPv6Address(networkAddress().value().or(hostMask)); } + /** + * Address at network base + offset. Offset must address a host within + * this prefix (not the anycast/base address). + */ + @NonNull + public IPv6Address addressAtOffset(@NonNull BigInteger offset) { + if (offset.signum() <= 0 || offset.compareTo(totalAddresses()) >= 0) + throw new IllegalArgumentException(fmt( + "Offset %s out of range for /%d network", offset, prefix + )); + return new IPv6Address(networkAddress().value().add(offset)); + } + + @NonNull + public IPv6Address addressAtOffset(long offset) { + return addressAtOffset(BigInteger.valueOf(offset)); + } + @Override public boolean contains(@NonNull IPv6Address ip) { var v = ip.value(); diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/network/BridgeType.java b/app/src/main/java/cn/classfun/droidvm/lib/store/network/BridgeType.java new file mode 100644 index 000000000..bd8f10da2 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/network/BridgeType.java @@ -0,0 +1,27 @@ +package cn.classfun.droidvm.lib.store.network; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public enum BridgeType { + LINUX("linux"), + GVISOR("gvisor"); + + private final String key; + + BridgeType(@NonNull String key) { + this.key = key; + } + + @NonNull + public String key() { + return key; + } + + @NonNull + public static BridgeType fromKey(@Nullable String key) { + for (var v : values()) + if (v.key.equals(key)) return v; + return LINUX; + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/network/Ipv6Source.java b/app/src/main/java/cn/classfun/droidvm/lib/store/network/Ipv6Source.java new file mode 100644 index 000000000..e27728bb2 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/network/Ipv6Source.java @@ -0,0 +1,27 @@ +package cn.classfun.droidvm.lib.store.network; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public enum Ipv6Source { + STATIC("static"), + DHCP_PD("dhcp_pd"); + + private final String key; + + Ipv6Source(@NonNull String key) { + this.key = key; + } + + @NonNull + public String key() { + return key; + } + + @NonNull + public static Ipv6Source fromKey(@Nullable String key) { + for (var v : values()) + if (v.key.equals(key)) return v; + return STATIC; + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/network/NetworkConfig.java b/app/src/main/java/cn/classfun/droidvm/lib/store/network/NetworkConfig.java index d7b4bc94b..c83b98e66 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/network/NetworkConfig.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/network/NetworkConfig.java @@ -1,20 +1,145 @@ package cn.classfun.droidvm.lib.store.network; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import org.json.JSONException; import org.json.JSONObject; +import java.util.ArrayList; +import java.util.List; import java.util.UUID; import cn.classfun.droidvm.lib.store.base.DataConfig; +import cn.classfun.droidvm.lib.store.base.DataItem; public class NetworkConfig extends DataConfig { + public static final int SCHEMA_VERSION = 2; + public NetworkConfig() { setId(UUID.randomUUID()); + item.set("schema", (long) SCHEMA_VERSION); } public NetworkConfig(@NonNull JSONObject obj) throws JSONException { item.set(obj); + if (!isSupportedSchema(obj)) throw new JSONException( + "Unsupported network config schema (expected " + SCHEMA_VERSION + ")" + ); + } + + public static boolean isSupportedSchema(@NonNull JSONObject obj) { + return obj.optInt("schema", 0) == SCHEMA_VERSION; + } + + @Nullable + public String getBridgeName() { + return item.optString("bridge_name", null); + } + + public void setBridgeName(@NonNull String name) { + item.set("bridge_name", name); + } + + @NonNull + public BridgeType getBridgeType() { + return BridgeType.fromKey(item.optString("bridge_type", null)); + } + + public void setBridgeType(@NonNull BridgeType type) { + item.set("bridge_type", type.key()); + } + + @NonNull + public UplinkMode getUplinkMode() { + return UplinkMode.fromKey(item.optString("uplink_mode", null)); + } + + public void setUplinkMode(@NonNull UplinkMode mode) { + item.set("uplink_mode", mode.key()); + } + + public boolean isStp() { + return item.optBoolean("stp", false); + } + + public boolean isAutoUp() { + return item.optBoolean("auto_up", false); + } + + @NonNull + private DataItem section(@NonNull String key) { + var s = item.opt(key, null); + if (s == null || !s.is(DataItem.Type.OBJECT)) { + // set() stores a copy; re-read so callers mutate the stored item + item.set(key, DataItem.newObject()); + s = item.get(key); + } + return s; + } + + @NonNull + public DataItem l2() { + return section("l2"); + } + + @NonNull + public DataItem l3() { + return section("l3"); + } + + /** + * L2 mode: configured uplink — a literal interface name or a + * "WiFi"/"Ethernet"/"Tethering" logical identifier resolved at start. + */ + @Nullable + public String getL2Uplink() { + return l2().optString("uplink", null); + } + + /** L2 mode: whether to pseudo-bridge (force-on for non-bridgeable uplinks). */ + public boolean isL2PseudoBridge() { + return l2().optBoolean("pseudo_bridge", true); + } + + /** MAC of the bridge itself for the current uplink mode, if configured. */ + @Nullable + public String getBridgeMacAddress() { + switch (getUplinkMode()) { + case L3: + return emptyToNull(l3().optString("mac_address", null)); + default: + return null; + } + } + + @NonNull + public List getVlans() { + var out = new ArrayList(); + if (getUplinkMode() != UplinkMode.L3) return out; + var arr = l3().opt("vlans", null); + if (arr == null || !arr.is(DataItem.Type.ARRAY)) return out; + for (var e : arr.asArray()) + if (e.is(DataItem.Type.OBJECT)) out.add(new VlanConfig(e)); + return out; + } + + @Nullable + public VlanConfig findVlan(int vlanId) { + for (var vlan : getVlans()) + if (vlan.getVlanId() == vlanId) return vlan; + return null; + } + + /** True if any VLAN id other than the untagged (0) domain is configured. */ + public boolean hasTaggedVlans() { + for (var vlan : getVlans()) + if (!vlan.isUntagged()) return true; + return false; + } + + @Nullable + private static String emptyToNull(@Nullable String s) { + return s == null || s.isEmpty() ? null : s; } } diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/network/NetworkConfigValidator.java b/app/src/main/java/cn/classfun/droidvm/lib/store/network/NetworkConfigValidator.java new file mode 100644 index 000000000..73e978dda --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/network/NetworkConfigValidator.java @@ -0,0 +1,181 @@ +package cn.classfun.droidvm.lib.store.network; + +import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.math.BigInteger; +import java.util.HashSet; + +import cn.classfun.droidvm.lib.network.IPv4Network; +import cn.classfun.droidvm.lib.network.IPv6Network; +import cn.classfun.droidvm.lib.utils.NetUtils; + +/** + * Validates a schema-v2 network config. Shared by the edit UI and the + * daemon's create/modify handlers. Throws {@link IllegalArgumentException} + * with a human-readable message on the first violation found. + */ +public final class NetworkConfigValidator { + private NetworkConfigValidator() { + } + + public static void validate(@NonNull NetworkConfig config) { + var name = config.getName(); + if (name == null || name.trim().isEmpty()) + throw new IllegalArgumentException("Network name must not be empty"); + var bridgeName = config.getBridgeName(); + if (bridgeName == null || !bridgeName.matches("[a-zA-Z][a-zA-Z0-9_-]*")) + throw new IllegalArgumentException(fmt("Invalid bridge name: %s", bridgeName)); + if (bridgeName.length() > 15) + throw new IllegalArgumentException("Bridge name longer than 15 characters"); + + var type = config.getBridgeType(); + var mode = config.getUplinkMode(); + switch (mode) { + case L2: + validateL2(config, type); + break; + case L3: + validateL3(config, type); + break; + } + } + + private static void validateL2(@NonNull NetworkConfig config, @NonNull BridgeType type) { + if (type != BridgeType.LINUX) + throw new IllegalArgumentException("L2 bridging requires a Linux bridge"); + var uplink = config.getL2Uplink(); + if (uplink == null || uplink.trim().isEmpty()) + throw new IllegalArgumentException("L2 bridging requires an uplink interface"); + // pseudo_bridge is a boolean toggle; nothing further to validate + } + + private static void validateL3(@NonNull NetworkConfig config, @NonNull BridgeType type) { + validateMac(config.getBridgeMacAddress()); + // zero VLANs is valid: a VM-internal network the host doesn't join + var vlans = config.getVlans(); + var seen = new HashSet(); + for (var vlan : vlans) { + int id = vlan.getVlanId(); + if (id < 0 || id > 4094) + throw new IllegalArgumentException(fmt("Invalid VLAN id: %d", id)); + if (!seen.add(id)) + throw new IllegalArgumentException(fmt("Duplicate VLAN id: %d", id)); + validateVlan(vlan, type); + } + } + + private static void validateVlan(@NonNull VlanConfig vlan, @NonNull BridgeType type) { + var label = vlan.isUntagged() ? "untagged" : String.valueOf(vlan.getVlanId()); + + var cidr4 = vlan.getIpv4Cidr(); + IPv4Network net4 = null; + if (cidr4 != null) { + try { + net4 = IPv4Network.parse(cidr4); + } catch (Exception e) { + throw new IllegalArgumentException(fmt( + "VLAN %s: invalid IPv4 CIDR: %s", label, cidr4 + )); + } + if (net4.prefix() > 30) throw new IllegalArgumentException(fmt( + "VLAN %s: IPv4 prefix /%d too small for a gateway network", + label, net4.prefix() + )); + if (net4.address().equals(net4.networkAddress()) + || net4.address().equals(net4.broadcastAddress())) + throw new IllegalArgumentException(fmt( + "VLAN %s: IPv4 gateway address must be a host address, not %s", + label, net4.address() + )); + } + if (vlan.isDhcp4Enabled()) { + if (net4 == null) throw new IllegalArgumentException(fmt( + "VLAN %s: DHCPv4 requires a primary IPv4 network", label + )); + validateOffsets(label, "DHCPv4", + vlan.getDhcp4OffsetStart(), vlan.getDhcp4OffsetEnd(), + BigInteger.valueOf(net4.totalAddresses() - 1)); + } + + var source = vlan.getIpv6Source(); + if (source == Ipv6Source.DHCP_PD) { + if (type != BridgeType.LINUX) throw new IllegalArgumentException(fmt( + "VLAN %s: DHCP-PD is only available on Linux bridges", label + )); + var uplink = vlan.getPdUplink(); + if (uplink == null || uplink.trim().isEmpty()) + throw new IllegalArgumentException(fmt( + "VLAN %s: DHCP-PD requires an uplink interface", label + )); + var duid = vlan.getPdDuid(); + if (duid != null && !duid.isEmpty() && !duid.matches("([0-9a-fA-F]{2}:)*[0-9a-fA-F]{2}")) + throw new IllegalArgumentException(fmt("VLAN %s: invalid DUID", label)); + } else { + var cidr6 = vlan.getIpv6Cidr(); + if (cidr6 != null) { + IPv6Network net6; + try { + net6 = IPv6Network.parse(cidr6); + } catch (Exception e) { + throw new IllegalArgumentException(fmt( + "VLAN %s: invalid IPv6 CIDR: %s", label, cidr6 + )); + } + if (net6.prefix() > 126) throw new IllegalArgumentException(fmt( + "VLAN %s: IPv6 prefix /%d too small", label, net6.prefix() + )); + if (vlan.isSlaacEnabled() && net6.prefix() != 64) + throw new IllegalArgumentException(fmt( + "VLAN %s: SLAAC requires a /64 network", label + )); + } + } + // ipv6.snat may be stored true on a Linux bridge: it has no effect + // there (the Android kernel has no IPv6 NAT) but is kept so the + // setting survives a round-trip through the Linux bridge type + if (vlan.isDhcp6Enabled()) { + if (!vlan.hasIpv6()) throw new IllegalArgumentException(fmt( + "VLAN %s: DHCPv6 requires a primary IPv6 network", label + )); + validateOffsets(label, "DHCPv6", + vlan.getDhcp6OffsetStart(), vlan.getDhcp6OffsetEnd(), null); + } + + if (type == BridgeType.GVISOR + && (!vlan.getIpv4Secondary().isEmpty() || !vlan.getIpv6Secondary().isEmpty())) + throw new IllegalArgumentException(fmt( + "VLAN %s: secondary networks are not supported on gVisor bridges", label + )); + for (var cidr : vlan.getIpv4Secondary()) + if (!IPv4Network.isValid(cidr)) throw new IllegalArgumentException(fmt( + "VLAN %s: invalid secondary IPv4 CIDR: %s", label, cidr + )); + for (var cidr : vlan.getIpv6Secondary()) + if (!IPv6Network.isValid(cidr)) throw new IllegalArgumentException(fmt( + "VLAN %s: invalid secondary IPv6 CIDR: %s", label, cidr + )); + } + + private static void validateOffsets( + @NonNull String label, @NonNull String what, + long start, long end, @Nullable BigInteger maxExclusive + ) { + if (start <= 0 || end < start) throw new IllegalArgumentException(fmt( + "VLAN %s: invalid %s pool offsets %d-%d", label, what, start, end + )); + if (maxExclusive != null + && BigInteger.valueOf(end).compareTo(maxExclusive) >= 0) + throw new IllegalArgumentException(fmt( + "VLAN %s: %s pool offset %d exceeds the network size", label, what, end + )); + } + + private static void validateMac(@Nullable String mac) { + if (mac == null) return; + if (!NetUtils.isValidMac(mac)) + throw new IllegalArgumentException(fmt("Invalid MAC address: %s", mac)); + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/network/NetworkStore.java b/app/src/main/java/cn/classfun/droidvm/lib/store/network/NetworkStore.java index af4eae2fa..0439cf1a7 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/network/NetworkStore.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/network/NetworkStore.java @@ -1,6 +1,7 @@ package cn.classfun.droidvm.lib.store.network; import android.content.Context; +import android.util.Log; import androidx.annotation.NonNull; @@ -10,6 +11,7 @@ import java.io.File; import cn.classfun.droidvm.lib.store.base.DataStore; +import cn.classfun.droidvm.lib.utils.JsonUtils; public final class NetworkStore extends DataStore { public NetworkStore() { @@ -31,6 +33,26 @@ public NetworkStore(@NonNull Context context) { super(context); } + @Override + protected boolean load(@NonNull DataStore store, @NonNull JSONObject obj) { + try { + store.clear(); + JsonUtils.forEachArray(obj, getTypeName(), (JSONObject entry) -> { + if (!NetworkConfig.isSupportedSchema(entry)) { + Log.w(TAG, "Skipping network config with unsupported schema: " + + entry.optString("name")); + return; + } + store.addObject(entry); + }); + return true; + } catch (Exception e) { + Log.e(TAG, "Failed to load network configs", e); + store.clear(); + return false; + } + } + @NonNull @Override protected NetworkConfig create() { diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/network/UplinkMode.java b/app/src/main/java/cn/classfun/droidvm/lib/store/network/UplinkMode.java new file mode 100644 index 000000000..b5a6d0136 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/network/UplinkMode.java @@ -0,0 +1,28 @@ +package cn.classfun.droidvm.lib.store.network; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public enum UplinkMode { + L2("l2"), + L3("l3"); + + private final String key; + + UplinkMode(@NonNull String key) { + this.key = key; + } + + @NonNull + public String key() { + return key; + } + + @NonNull + public static UplinkMode fromKey(@Nullable String key) { + for (var v : values()) + if (v.key.equals(key)) return v; + // legacy/unknown keys (incl. the dropped "none" mode) read as host L3 + return L3; + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/network/VlanConfig.java b/app/src/main/java/cn/classfun/droidvm/lib/store/network/VlanConfig.java new file mode 100644 index 000000000..9f70591f5 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/network/VlanConfig.java @@ -0,0 +1,235 @@ +package cn.classfun.droidvm.lib.store.network; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.ArrayList; +import java.util.List; + +import cn.classfun.droidvm.lib.network.IPv4Network; +import cn.classfun.droidvm.lib.network.IPv6Network; +import cn.classfun.droidvm.lib.store.base.DataItem; + +/** + * Wrapper over one entry of an L3 network's "vlans" array. + * VLAN id 0 means the untagged domain. + */ +public final class VlanConfig { + public final DataItem item; + + public VlanConfig(@NonNull DataItem item) { + this.item = item; + } + + @NonNull + public static VlanConfig createDefault(int vlanId) { + var item = DataItem.newObject(); + item.set("vlan_id", (long) vlanId); + var ipv4 = DataItem.newObject(); + var dhcp4 = DataItem.newObject(); + dhcp4.set("enabled", true); + dhcp4.set("offset_start", 128L); + dhcp4.set("offset_end", 192L); + ipv4.set("cidr", (String) null); + ipv4.set("snat", true); + ipv4.set("dhcp", dhcp4); + item.set("ipv4", ipv4); + var ipv6 = DataItem.newObject(); + var dhcp6 = DataItem.newObject(); + dhcp6.set("enabled", true); + dhcp6.set("offset_start", 128L); + dhcp6.set("offset_end", 192L); + var slaac = DataItem.newObject(); + slaac.set("enabled", true); + ipv6.set("source", Ipv6Source.STATIC.key()); + ipv6.set("cidr", (String) null); + ipv6.set("snat", false); + ipv6.set("dhcp", dhcp6); + ipv6.set("slaac", slaac); + item.set("ipv6", ipv6); + item.set("dns_servers", DataItem.newArray()); + item.set("ipv4_secondary", DataItem.newArray()); + item.set("ipv6_secondary", DataItem.newArray()); + return new VlanConfig(item); + } + + public int getVlanId() { + return (int) item.optLong("vlan_id", 0); + } + + public boolean isUntagged() { + return getVlanId() == 0; + } + + @NonNull + private DataItem section(@NonNull String key) { + var s = item.opt(key, null); + if (s == null || !s.is(DataItem.Type.OBJECT)) { + // set() stores a copy; re-read so callers mutate the stored item + item.set(key, DataItem.newObject()); + s = item.get(key); + } + return s; + } + + @NonNull + public DataItem ipv4() { + return section("ipv4"); + } + + @NonNull + public DataItem ipv6() { + return section("ipv6"); + } + + @Nullable + public String getIpv4Cidr() { + var cidr = ipv4().optString("cidr", null); + return cidr == null || cidr.isEmpty() ? null : cidr; + } + + @Nullable + public IPv4Network getIpv4Network() { + var cidr = getIpv4Cidr(); + if (cidr == null) return null; + try { + return IPv4Network.parse(cidr); + } catch (Exception e) { + return null; + } + } + + public boolean isIpv4Snat() { + return ipv4().optBoolean("snat", false); + } + + public boolean isDhcp4Enabled() { + return getIpv4Cidr() != null + && ipv4().opt("dhcp", DataItem.newObject()).optBoolean("enabled", false); + } + + public long getDhcp4OffsetStart() { + return ipv4().opt("dhcp", DataItem.newObject()).optLong("offset_start", 128); + } + + public long getDhcp4OffsetEnd() { + return ipv4().opt("dhcp", DataItem.newObject()).optLong("offset_end", 192); + } + + @NonNull + public Ipv6Source getIpv6Source() { + return Ipv6Source.fromKey(ipv6().optString("source", Ipv6Source.STATIC.key())); + } + + @Nullable + public String getIpv6Cidr() { + var cidr = ipv6().optString("cidr", null); + return cidr == null || cidr.isEmpty() ? null : cidr; + } + + @Nullable + public IPv6Network getIpv6Network() { + if (getIpv6Source() != Ipv6Source.STATIC) return null; + var cidr = getIpv6Cidr(); + if (cidr == null) return null; + try { + return IPv6Network.parse(cidr); + } catch (Exception e) { + return null; + } + } + + public boolean hasIpv6() { + return getIpv6Source() == Ipv6Source.DHCP_PD || getIpv6Cidr() != null; + } + + @Nullable + public String getPdUplink() { + return ipv6().opt("pd", DataItem.newObject()).optString("uplink", null); + } + + @Nullable + public String getPdDuid() { + return ipv6().opt("pd", DataItem.newObject()).optString("duid", null); + } + + public void setPdDuid(@NonNull String duid) { + pdSection().set("duid", duid); + } + + /** + * Persisted routing-table id for bridgedhcp's routed-PD mode; 0 = not + * yet allocated. The id is bridgedhcp's crash-cleanup key, so once + * allocated it must stay stable for this VLAN across restarts. + */ + public long getPdRouteTable() { + return ipv6().opt("pd", DataItem.newObject()).optLong("route_table", 0); + } + + public void setPdRouteTable(long table) { + pdSection().set("route_table", table); + } + + /** The live (mutable) "pd" object of the ipv6 section, created on demand. */ + @NonNull + private DataItem pdSection() { + var ipv6 = ipv6(); + var pd = ipv6.opt("pd", null); + if (pd == null || !pd.is(DataItem.Type.OBJECT)) { + // set() stores a copy; re-read so callers mutate the stored item + ipv6.set("pd", DataItem.newObject()); + pd = ipv6.get("pd"); + } + return pd; + } + + public boolean isIpv6Snat() { + return ipv6().optBoolean("snat", false); + } + + public boolean isDhcp6Enabled() { + return hasIpv6() + && ipv6().opt("dhcp", DataItem.newObject()).optBoolean("enabled", false); + } + + public long getDhcp6OffsetStart() { + return ipv6().opt("dhcp", DataItem.newObject()).optLong("offset_start", 128); + } + + public long getDhcp6OffsetEnd() { + return ipv6().opt("dhcp", DataItem.newObject()).optLong("offset_end", 192); + } + + public boolean isSlaacEnabled() { + return hasIpv6() + && ipv6().opt("slaac", DataItem.newObject()).optBoolean("enabled", false); + } + + @NonNull + public List getDnsServers() { + return stringList("dns_servers"); + } + + @NonNull + public List getIpv4Secondary() { + return stringList("ipv4_secondary"); + } + + @NonNull + public List getIpv6Secondary() { + return stringList("ipv6_secondary"); + } + + @NonNull + private List stringList(@NonNull String key) { + var out = new ArrayList(); + var arr = item.opt(key, null); + if (arr == null || !arr.is(DataItem.Type.ARRAY)) return out; + for (var e : arr.asArray()) { + if (!e.is(DataItem.Type.STRING)) continue; + var s = e.asString(); + if (s != null && !s.isEmpty()) out.add(s); + } + return out; + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMConfig.java b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMConfig.java index a84d3be55..103d52e4d 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMConfig.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMConfig.java @@ -8,8 +8,10 @@ import org.json.JSONObject; import java.util.UUID; +import java.util.function.Consumer; import cn.classfun.droidvm.lib.store.base.DataConfig; +import cn.classfun.droidvm.lib.store.base.DataItem; public class VMConfig extends DataConfig { public VMConfig() { @@ -23,4 +25,13 @@ public VMConfig(@NonNull JSONObject obj) throws JSONException { item.remove("kernel"); } } + + /** Iterates this VM's NIC entries (the "networks" array). */ + public final void forEachNic(@NonNull Consumer consumer) { + var nets = item.opt("networks", null); + if (nets == null || !nets.is(DataItem.Type.ARRAY)) return; + for (var entry : nets.asArray()) + if (entry.is(DataItem.Type.OBJECT)) + consumer.accept(new VMNicConfig(entry)); + } } diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMNicConfig.java b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMNicConfig.java new file mode 100644 index 000000000..1b3592c05 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMNicConfig.java @@ -0,0 +1,322 @@ +package cn.classfun.droidvm.lib.store.vm; + +import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.json.JSONException; + +import java.util.ArrayList; +import java.util.List; + +import cn.classfun.droidvm.lib.store.base.DataItem; +import cn.classfun.droidvm.lib.store.network.BridgeType; +import cn.classfun.droidvm.lib.store.network.NetworkConfig; +import cn.classfun.droidvm.lib.store.network.UplinkMode; +import cn.classfun.droidvm.lib.store.network.VlanConfig; +import cn.classfun.droidvm.lib.utils.NetUtils; + +/** + * Wrapper over one entry of a VM config's "networks" array (one NIC). + */ +public final class VMNicConfig { + public final DataItem item; + + public VMNicConfig(@NonNull DataItem item) { + this.item = item; + } + + @Nullable + public String getNetworkId() { + var id = item.optString("network_id", null); + return id == null || id.isEmpty() ? null : id; + } + + @Nullable + public String getTapName() { + var tap = item.optString("tap_name", null); + return tap == null || tap.isEmpty() ? null : tap; + } + + public void setTapName(@NonNull String tapName) { + item.set("tap_name", tapName); + } + + @Nullable + public String getMacAddress() { + var mac = item.optString("mac_address", null); + return mac == null || mac.isEmpty() ? null : mac; + } + + public boolean isMacSecurity() { + return item.optBoolean("mac_security", false); + } + + public boolean isIsolated() { + return item.optBoolean("isolated", false); + } + + /** Access VLAN id; null means trunk (all VLANs). */ + @Nullable + public Integer getVlanId() { + var v = item.opt("vlan_id", null); + if (v == null || !v.is(DataItem.Type.INTEGER)) return null; + return (int) v.asInteger(); + } + + @NonNull + private DataItem lease(@NonNull String key) { + var s = item.opt(key, null); + if (s == null || !s.is(DataItem.Type.OBJECT)) { + // set() stores a copy; re-read so callers mutate the stored item + item.set(key, DataItem.newObject()); + s = item.get(key); + } + return s; + } + + public boolean isDhcp4LeaseEnabled() { + return lease("dhcp4_lease").optBoolean("enabled", false); + } + + public long getDhcp4Offset() { + return lease("dhcp4_lease").optLong("offset", 64); + } + + public boolean isDhcp6LeaseEnabled() { + return lease("dhcp6_lease").optBoolean("enabled", false); + } + + public long getDhcp6Offset() { + return lease("dhcp6_lease").optLong("offset", 64); + } + + @NonNull + public List getDhcp4Forwards() { + return forwards("dhcp4_lease"); + } + + @NonNull + public List getDhcp6Forwards() { + return forwards("dhcp6_lease"); + } + + @NonNull + private List forwards(@NonNull String key) { + var out = new ArrayList(); + var arr = lease(key).opt("forwards", null); + if (arr == null || !arr.is(DataItem.Type.ARRAY)) return out; + for (var e : arr.asArray()) { + if (!e.is(DataItem.Type.OBJECT)) continue; + out.add(new PortForward( + e.optString("proto", "tcp"), + e.optString("host", ""), + e.optString("guest", "") + )); + } + return out; + } + + /** + * The VLAN of this NIC the DHCP services apply to: the pinned access + * VLAN, or the untagged domain (VLAN 0) when trunked — either the + * implicit trunk (no vlan_id) or the explicit 4095 trunk marker. + */ + @Nullable + public VlanConfig resolveDhcpVlan(@NonNull NetworkConfig network) { + if (network.getUplinkMode() != UplinkMode.L3) return null; + var vlanId = getVlanId(); + if (vlanId == null || vlanId == 4095) vlanId = 0; + return network.findVlan(vlanId); + } + + /** + * Validates this NIC against its network config. Throws + * {@link IllegalArgumentException} on the first violation. + */ + public void validate(@NonNull NetworkConfig network) { + var mac = getMacAddress(); + if (mac != null && !NetUtils.isValidMac(mac)) + throw new IllegalArgumentException(fmt("Invalid NIC MAC address: %s", mac)); + if (isMacSecurity() && mac == null) + throw new IllegalArgumentException("MAC security requires a configured MAC address"); + var vlanId = getVlanId(); + if (vlanId != null) { + // Port semantics: 0 = untagged-only, 4095 = trunk marker + // (equivalent to no vlan_id), 1..4094 = tagged access. + if (vlanId < 0 || vlanId > 4095) + throw new IllegalArgumentException(fmt("Invalid NIC VLAN id: %d", vlanId)); + // An untagged-only port needs per-port VLAN filtering, which a + // Linux bridge on stock GKI cannot do — attaching to the main + // bridge would actually be a trunk (flooded tagged frames + // included). Refuse instead of silently degrading; gvisor's + // switchcore can enforce untagged-only, so 0 stays valid there. + if (vlanId == 0 && network.getBridgeType() == BridgeType.LINUX) + throw new IllegalArgumentException( + "Untagged-only (VLAN 0) ports are not supported on a Linux " + + "bridge; use trunk or a tagged VLAN"); + // An access VLAN not present in the network config is allowed: it + // is created on demand as an L2-only segment when the NIC attaches. + } + var vlan = resolveDhcpVlan(network); + if (isDhcp4LeaseEnabled()) { + if (vlan == null || !vlan.isDhcp4Enabled()) + throw new IllegalArgumentException( + "DHCPv4 static lease requires an L3 network with DHCPv4 enabled" + ); + if (mac == null) + throw new IllegalArgumentException("DHCPv4 static lease requires a MAC address"); + var net4 = vlan.getIpv4Network(); + if (net4 != null) net4.addressAtOffset(getDhcp4Offset()); + for (var fwd : getDhcp4Forwards()) { + fwd.validate(); + if (!vlan.isIpv4Snat()) throw new IllegalArgumentException( + "IPv4 port forwards require IPv4 SNAT on the network" + ); + } + } + if (isDhcp6LeaseEnabled()) { + if (vlan == null || !vlan.isDhcp6Enabled()) + throw new IllegalArgumentException( + "DHCPv6 static lease requires an L3 network with DHCPv6 enabled" + ); + var net6 = vlan.getIpv6Network(); + if (net6 != null) net6.addressAtOffset(getDhcp6Offset()); + for (var fwd : getDhcp6Forwards()) { + fwd.validate(); + // snat may be stored true on a Linux bridge but only takes + // effect on gVisor, so require the bridge type as well + if (!vlan.isIpv6Snat() + || network.getBridgeType() != BridgeType.GVISOR) + throw new IllegalArgumentException( + "IPv6 port forwards require IPv6 SNAT on a gVisor network" + ); + } + } + } + + /** + * An in-memory copy of this NIC with every option the target network can't + * honour stripped out; the saved config is left untouched. A NIC keeps its + * L3-only settings (DHCP static lease, port forwards) in the stored config + * so they survive being moved back to an L3 network — the GUI just hides + * them elsewhere. On an L2 / non-DHCP network those settings are inert, so + * for this run we drop them rather than refuse to start. Genuine + * misconfigurations are left in place for {@link #validate} to reject. + * + * @param dropped collects a human-readable note for each option removed + */ + @NonNull + public VMNicConfig sanitizedFor(@NonNull NetworkConfig network, @NonNull List dropped) { + DataItem copy; + try { + copy = DataItem.from(item.toJson()); // deep, independent copy + } catch (JSONException e) { + throw new IllegalStateException("NIC config is not serializable", e); + } + var sane = new VMNicConfig(copy); + var vlan = sane.resolveDhcpVlan(network); + // A DHCP static lease (and any forwards riding on it) only means + // something on an L3 network that actually serves that DHCP family. + if (sane.isDhcp4LeaseEnabled() && (vlan == null || !vlan.isDhcp4Enabled())) { + copy.remove("dhcp4_lease"); + dropped.add("DHCPv4 static lease"); + } else if (sane.isDhcp4LeaseEnabled() && !sane.getDhcp4Forwards().isEmpty() + && !vlan.isIpv4Snat()) { + dropForwards(copy, "dhcp4_lease"); + dropped.add("IPv4 port forwards (network has no IPv4 SNAT)"); + } + if (sane.isDhcp6LeaseEnabled() && (vlan == null || !vlan.isDhcp6Enabled())) { + copy.remove("dhcp6_lease"); + dropped.add("DHCPv6 static lease"); + } else if (sane.isDhcp6LeaseEnabled() && !sane.getDhcp6Forwards().isEmpty() + && (!vlan.isIpv6Snat() || network.getBridgeType() != BridgeType.GVISOR)) { + dropForwards(copy, "dhcp6_lease"); + dropped.add("IPv6 port forwards (require a gVisor network)"); + } + return sane; + } + + /** Drops the forwards array from a lease object, keeping the lease itself. */ + private static void dropForwards(@NonNull DataItem nic, @NonNull String leaseKey) { + var lease = nic.opt(leaseKey, null); + if (lease != null && lease.is(DataItem.Type.OBJECT)) lease.remove("forwards"); + } + + /** One "proto host:guest" port forward entry; ports may be N or N-M ranges. */ + public static final class PortForward { + public final String proto; + public final String host; + public final String guest; + + public PortForward(@NonNull String proto, @NonNull String host, @NonNull String guest) { + this.proto = proto; + this.host = host; + this.guest = guest; + } + + public int hostStart() { + return rangeStart(host); + } + + public int hostEnd() { + return rangeEnd(host); + } + + public int guestStart() { + return rangeStart(guest); + } + + public int guestEnd() { + return rangeEnd(guest); + } + + /** Concrete protocols this entry installs: "any" expands to tcp+udp. */ + @NonNull + public List protocols() { + if (proto.equals("any")) return List.of("tcp", "udp"); + return List.of(proto); + } + + public void validate() { + if (!proto.equals("tcp") && !proto.equals("udp") && !proto.equals("any")) + throw new IllegalArgumentException(fmt("Invalid forward protocol: %s", proto)); + int hs = parsePort(host, true), he = parsePort(host, false); + int gs = parsePort(guest, true), ge = parsePort(guest, false); + if (he < hs || ge < gs) + throw new IllegalArgumentException(fmt( + "Invalid port range in forward %s -> %s", host, guest + )); + if (he - hs != ge - gs) + throw new IllegalArgumentException(fmt( + "Forward port ranges differ in size: %s -> %s", host, guest + )); + } + + private static int rangeStart(@NonNull String spec) { + return parsePort(spec, true); + } + + private static int rangeEnd(@NonNull String spec) { + return parsePort(spec, false); + } + + private static int parsePort(@NonNull String spec, boolean start) { + var s = spec.trim(); + int dash = s.indexOf('-'); + String part; + if (dash < 0) part = s; + else part = start ? s.substring(0, dash) : s.substring(dash + 1); + int port; + try { + port = Integer.parseInt(part.trim()); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(fmt("Invalid port: %s", spec)); + } + if (port < 1 || port > 65535) + throw new IllegalArgumentException(fmt("Port out of range: %s", spec)); + return port; + } + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/lib/utils/NetUtils.java b/app/src/main/java/cn/classfun/droidvm/lib/utils/NetUtils.java index f1d2e7d19..8db1971dd 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/utils/NetUtils.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/utils/NetUtils.java @@ -228,6 +228,10 @@ public static String generateRandomMac() { ); } + public static boolean isValidMac(@Nullable String mac) { + return mac != null && mac.matches("([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}"); + } + @NonNull public static String stripScopeId(@NonNull String addr) { int i = addr.indexOf('%'); diff --git a/app/src/main/java/cn/classfun/droidvm/ui/main/network/NetworkAdapter.java b/app/src/main/java/cn/classfun/droidvm/ui/main/network/NetworkAdapter.java index 88630c656..c039e8fed 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/main/network/NetworkAdapter.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/main/network/NetworkAdapter.java @@ -25,13 +25,25 @@ public int getIconResId() { public void onBindViewHolder(@NonNull BaseViewHolder holder, int position) { var ctx = holder.itemView.getContext(); var config = items.get(position); - var addrCount = config.item.get("ipv4_addresses").size() - + config.item.get("ipv6_addresses").size(); + String typeLabel = ctx.getString( + config.getBridgeType() == cn.classfun.droidvm.lib.store.network.BridgeType.GVISOR + ? R.string.network_edit_bridge_type_gvisor + : R.string.network_edit_bridge_type_linux); + String modeLabel; + switch (config.getUplinkMode()) { + case L2: + modeLabel = ctx.getString(R.string.network_edit_uplink_l2); + break; + case L3: + modeLabel = ctx.getString(R.string.network_edit_uplink_l3); + break; + default: + modeLabel = ctx.getString(R.string.network_edit_uplink_none); + break; + } holder.itemInfo.setVisibility(VISIBLE); - holder.itemInfo.setText(ctx.getResources().getQuantityString( - R.plurals.network_item_info, addrCount, - config.item.optString("bridge_name", ""), addrCount - )); + holder.itemInfo.setText(ctx.getString( + R.string.network_item_subtitle, typeLabel, modeLabel)); super.onBindViewHolder(holder, position); } } diff --git a/app/src/main/java/cn/classfun/droidvm/ui/network/edit/NetworkEditActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/network/edit/NetworkEditActivity.java index 42745bb5c..2607bbaf8 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/network/edit/NetworkEditActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/network/edit/NetworkEditActivity.java @@ -4,15 +4,13 @@ import static android.view.View.VISIBLE; import static android.widget.Toast.LENGTH_LONG; import static android.widget.Toast.LENGTH_SHORT; -import static java.util.Objects.requireNonNull; import static cn.classfun.droidvm.lib.utils.NetUtils.generateRandomMac; import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; import android.os.Bundle; -import android.text.Editable; -import android.view.Gravity; -import android.view.ViewGroup; -import android.widget.ImageButton; +import android.os.Handler; +import android.os.Looper; +import android.view.LayoutInflater; import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; @@ -22,81 +20,89 @@ import androidx.appcompat.app.AppCompatActivity; import com.google.android.material.appbar.CollapsingToolbarLayout; +import com.google.android.material.button.MaterialButton; import com.google.android.material.floatingactionbutton.FloatingActionButton; import java.util.ArrayList; import java.util.List; -import java.util.Locale; import java.util.Random; import java.util.UUID; import cn.classfun.droidvm.R; +import cn.classfun.droidvm.lib.daemon.DaemonConnection; import cn.classfun.droidvm.lib.network.IPv4Address; import cn.classfun.droidvm.lib.network.IPv4Network; import cn.classfun.droidvm.lib.network.IPv6Network; import cn.classfun.droidvm.lib.store.base.DataItem; +import cn.classfun.droidvm.daemon.network.backend.UplinkResolver; +import cn.classfun.droidvm.lib.store.network.BridgeType; +import cn.classfun.droidvm.lib.store.network.Ipv6Source; import cn.classfun.droidvm.lib.store.network.NetworkConfig; +import cn.classfun.droidvm.lib.store.network.NetworkConfigValidator; import cn.classfun.droidvm.lib.store.network.NetworkStore; +import cn.classfun.droidvm.lib.store.network.UplinkMode; +import cn.classfun.droidvm.lib.store.network.VlanConfig; import cn.classfun.droidvm.lib.ui.BackAskHelper; -import cn.classfun.droidvm.lib.ui.SimpleTextWatcher; +import cn.classfun.droidvm.lib.ui.IconItemAdapter; +import cn.classfun.droidvm.ui.widgets.row.DropdownRowWidget; import cn.classfun.droidvm.ui.widgets.row.SwitchRowWidget; import cn.classfun.droidvm.ui.widgets.row.TextInputRowWidget; public final class NetworkEditActivity extends AppCompatActivity { public static final String EXTRA_NETWORK_ID = "network_id"; - private final List ipv4Addresses = new ArrayList<>(); - private final List ipv6Addresses = new ArrayList<>(); - private final List dnsServers = new ArrayList<>(); + private final Handler mainHandler = new Handler(Looper.getMainLooper()); + private final Random random = new Random(); + private final List vlans = new ArrayList<>(); + private final List binders = new ArrayList<>(); + // parallel uplink picker entries: display label, stored value (logical id + // or literal name) and whether that uplink can be enslaved into a bridge + private final List uplinkLabels = new ArrayList<>(); + private final List uplinkValues = new ArrayList<>(); + private final List uplinkBridgeable = new ArrayList<>(); + private String selectedUplink = UplinkResolver.ID_WIFI; private boolean editMode = false; private UUID editNetworkId = null; + private String existingBridgeName = null; private CollapsingToolbarLayout collapsingToolbar; - private TextInputRowWidget inputName, inputBridgeName, inputMac; - private SwitchRowWidget swAutoUp, swStp, swNat, swDhcp; - private TextInputRowWidget inputIPv4, inputIPv6, inputDns; - private LinearLayout layoutIPv4, layoutIPv6, layoutDns, groupDhcp; - private TextView tvIPv4Empty, tvIPv6Empty, tvDnsEmpty; - private TextInputRowWidget inputDhcpStart, inputDhcpEnd; + private TextView tvRunningBanner; + private TextInputRowWidget inputName; + private SwitchRowWidget swAutoUp, swStp; + private DropdownRowWidget ddBridgeType, ddUplinkMode; + private DropdownRowWidget ddL2Uplink; + private SwitchRowWidget swPseudoBridge; + private TextView tvPseudoHint; + private LinearLayout groupL2, groupL3; + private TextInputRowWidget inputMac; + private LinearLayout containerVlans; + private TextView tvVlanEmpty; + private MaterialButton btnAddVlan; private FloatingActionButton fab; private NetworkStore store; - @SuppressWarnings("unused") - private boolean macManuallyEdited = false; - @SuppressWarnings("unused") - private boolean bridgeManuallyEdited = false; - @SuppressWarnings("unused") - private boolean ipv4ManuallyEdited = false; - @SuppressWarnings("unused") - private boolean dhcpStartManuallyEdited = false; - @SuppressWarnings("unused") - private boolean dhcpEndManuallyEdited = false; - @SuppressWarnings("unused") - private boolean programmaticChange = false; - - private final Random random = new Random(); + private String[] bridgeTypeLabels; + private String[] uplinkModeLabels; + private BridgeType bridgeType = BridgeType.LINUX; + private UplinkMode uplinkMode = UplinkMode.L3; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_network_edit); collapsingToolbar = findViewById(R.id.collapsing_toolbar); + tvRunningBanner = findViewById(R.id.tv_running_banner); inputName = findViewById(R.id.input_name); - inputBridgeName = findViewById(R.id.input_bridge_name); - inputMac = findViewById(R.id.input_mac); swAutoUp = findViewById(R.id.sw_auto_up); swStp = findViewById(R.id.sw_stp); - swNat = findViewById(R.id.sw_nat); - swDhcp = findViewById(R.id.sw_dhcp); - inputIPv4 = findViewById(R.id.input_ipv4); - inputIPv6 = findViewById(R.id.input_ipv6); - inputDns = findViewById(R.id.input_dns); - layoutIPv4 = findViewById(R.id.layout_ipv4); - layoutIPv6 = findViewById(R.id.layout_ipv6); - layoutDns = findViewById(R.id.layout_dns); - tvIPv4Empty = findViewById(R.id.tv_ipv4_empty); - tvIPv6Empty = findViewById(R.id.tv_ipv6_empty); - tvDnsEmpty = findViewById(R.id.tv_dns_empty); - groupDhcp = findViewById(R.id.group_dhcp); - inputDhcpStart = findViewById(R.id.input_dhcp_start); - inputDhcpEnd = findViewById(R.id.input_dhcp_end); + ddBridgeType = findViewById(R.id.dd_bridge_type); + ddUplinkMode = findViewById(R.id.dd_uplink_mode); + groupL2 = findViewById(R.id.group_l2); + ddL2Uplink = findViewById(R.id.dd_l2_uplink); + swPseudoBridge = findViewById(R.id.sw_pseudo_bridge); + tvPseudoHint = findViewById(R.id.tv_pseudo_hint); + groupL3 = findViewById(R.id.group_l3); + inputMac = findViewById(R.id.input_mac); + containerVlans = findViewById(R.id.container_vlans); + tvVlanEmpty = findViewById(R.id.tv_vlan_empty); + btnAddVlan = findViewById(R.id.btn_add_vlan); fab = findViewById(R.id.fab_save); initialize(); } @@ -105,160 +111,303 @@ private void initialize() { new BackAskHelper(this); store = new NetworkStore(); store.load(this); - inputIPv4.setEndIconOnClickListener(v -> onAddIPv4()); - inputIPv6.setEndIconOnClickListener(v -> onAddIPv6()); - inputDns.setEndIconOnClickListener(v -> onAddDns()); - inputIPv4.addTextChangedListener(new SimpleTextWatcher() { - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - inputIPv4.setError(null); - } + bridgeTypeLabels = new String[]{ + getString(R.string.network_edit_bridge_type_linux), + getString(R.string.network_edit_bridge_type_gvisor), + }; + uplinkModeLabels = new String[]{ + getString(R.string.network_edit_uplink_l2), + getString(R.string.network_edit_uplink_l3), + }; + ddBridgeType.setAdapter(IconItemAdapter.create( + this, bridgeTypeLabels, R.drawable.ic_switch)); + ddBridgeType.setOnItemClickListener((p, v, pos, id) -> { + bridgeType = pos == 1 ? BridgeType.GVISOR : BridgeType.LINUX; + onBridgeTypeChanged(); }); - inputIPv6.addTextChangedListener(new SimpleTextWatcher() { - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - inputIPv6.setError(null); - } - }); - inputDns.addTextChangedListener(new SimpleTextWatcher() { - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - inputDns.setError(null); - } - }); - swDhcp.setOnCheckedChangeListener((btn, checked) -> - groupDhcp.setVisibility(checked ? VISIBLE : GONE)); + ddL2Uplink.setOnItemClickListener((p, v, pos, id) -> onUplinkSelected(pos)); + btnAddVlan.setOnClickListener(v -> onAddVlan()); + inputMac.setEndIconOnClickListener(v -> inputMac.setText(generateRandomMac())); fab.setOnClickListener(v -> onSaveClicked()); - installManualEditWatchers(); + updateUplinkModeDropdown(); + loadUplinks(); var intent = getIntent(); if (intent != null && intent.hasExtra(EXTRA_NETWORK_ID)) { editMode = true; editNetworkId = UUID.fromString(intent.getStringExtra(EXTRA_NETWORK_ID)); collapsingToolbar.setTitle(getString(R.string.network_edit_title)); loadExistingConfig(); + checkRunningState(); } else { collapsingToolbar.setTitle(getString(R.string.network_create_title)); generateDefaults(); } - buildAddressList(layoutIPv4, tvIPv4Empty, ipv4Addresses, this::updateDhcpFromIPv4); - buildAddressList(layoutIPv6, tvIPv6Empty, ipv6Addresses, null); - buildAddressList(layoutDns, tvDnsEmpty, dnsServers, null); + ddBridgeType.setText( + bridgeType == BridgeType.GVISOR ? bridgeTypeLabels[1] : bridgeTypeLabels[0]); + applyUplinkMode(); + rebuildVlanCards(); } - private void installManualEditWatchers() { - inputMac.addTextChangedListener(new ManualEditWatcher() { - @Override - protected void onManualEdit() { - macManuallyEdited = true; - propagateMacToBridge(); - } - }); - inputBridgeName.addTextChangedListener(new ManualEditWatcher() { - @Override - protected void onManualEdit() { - bridgeManuallyEdited = true; - } - }); - inputDhcpStart.addTextChangedListener(new ManualEditWatcher() { - @Override - protected void onManualEdit() { - dhcpStartManuallyEdited = true; - } - }); - inputDhcpEnd.addTextChangedListener(new ManualEditWatcher() { - @Override - protected void onManualEdit() { - dhcpEndManuallyEdited = true; - } + private void updateUplinkModeDropdown() { + // L2 bridging is Linux-only; "none" is just L3 with zero VLANs + var labels = bridgeType == BridgeType.GVISOR + ? new String[]{uplinkModeLabels[1]} + : uplinkModeLabels; + ddUplinkMode.setAdapter(IconItemAdapter.create( + this, labels, R.drawable.ic_web_plus)); + ddUplinkMode.setOnItemClickListener((p, v, pos, id) -> { + uplinkMode = labels[pos].equals(uplinkModeLabels[0]) + ? UplinkMode.L2 : UplinkMode.L3; + applyUplinkMode(); }); + ddUplinkMode.setText(labelOfMode(uplinkMode)); } - private abstract class ManualEditWatcher extends SimpleTextWatcher { - @Override - public void afterTextChanged(Editable s) { - if (!programmaticChange) onManualEdit(); - } - - protected abstract void onManualEdit(); + @NonNull + private String labelOfMode(@NonNull UplinkMode mode) { + return mode == UplinkMode.L2 ? uplinkModeLabels[0] : uplinkModeLabels[1]; } - private void generateDefaults() { - var mac = generateRandomMac(); - setTextProgrammatic(inputMac, mac); - setTextProgrammatic(inputBridgeName, bridgeNameFromMac(mac)); - var ipv4Cidr = generateRandomIPv4(); - ipv4Addresses.add(ipv4Cidr); - addDefaultDnsServers(); - swDhcp.setChecked(true); - updateDhcpFromIPv4(); + private void onBridgeTypeChanged() { + if (bridgeType == BridgeType.GVISOR && uplinkMode == UplinkMode.L2) + uplinkMode = UplinkMode.L3; + updateUplinkModeDropdown(); + applyUplinkMode(); + for (var binder : binders) + binder.applyBridgeType(bridgeType); } - private void addDefaultDnsServers() { - var a = IPv4Address.parse("8.8.8.8"); - var b = IPv4Address.parse("1.1.1.1"); - if (a != null) dnsServers.add(a); - if (b != null) dnsServers.add(b); + private void applyUplinkMode() { + groupL2.setVisibility(uplinkMode == UplinkMode.L2 ? VISIBLE : GONE); + groupL3.setVisibility(uplinkMode == UplinkMode.L3 ? VISIBLE : GONE); + updatePseudoHint(); } - @NonNull - private String bridgeNameFromMac(@NonNull String mac) { - return fmt("br%s", mac.replace(":", "").toLowerCase()); + private void loadUplinks() { + rebuildUplinks(null); + DaemonConnection.getInstance().buildRequest("network_list_uplinks") + .onResponse(resp -> { + var data = resp.optJSONObject("data"); + mainHandler.post(() -> { + rebuildUplinks(data); + for (var binder : binders) + binder.applyBridgeType(bridgeType); + }); + }) + .onUnsuccessful(r -> { + }) + .onError(e -> { + }) + .invoke(); } - @NonNull - private IPv4Network generateRandomIPv4() { - var existing = new ArrayList(); - store.forEach((id, cfg) -> { - if (id.equals(editNetworkId)) return; - cfg.item.get("ipv4_addresses").forEachArray(a -> { - try { - existing.add(IPv4Network.parse(a.asString())); - } catch (Exception ignored) { + /** + * Rebuilds the picker entries: the three logical identifiers (always shown, + * with their live-resolved name or "unavailable") followed by the concrete + * physical L2 devices. The current selection is preserved. + */ + private void rebuildUplinks(@Nullable org.json.JSONObject data) { + uplinkLabels.clear(); + uplinkValues.clear(); + uplinkBridgeable.clear(); + addIdentifier(UplinkResolver.ID_WIFI, + getString(R.string.network_edit_uplink_wifi), false, data); + addIdentifier(UplinkResolver.ID_ETHERNET, + getString(R.string.network_edit_uplink_ethernet), true, data); + addIdentifier(UplinkResolver.ID_TETHERING, + getString(R.string.network_edit_uplink_tethering), true, data); + if (data != null) { + var devices = data.optJSONArray("devices"); + if (devices != null) + for (int i = 0; i < devices.length(); i++) { + var obj = devices.optJSONObject(i); + if (obj == null) continue; + var name = obj.optString("name", ""); + if (name.isEmpty() || uplinkValues.contains(name)) continue; + uplinkLabels.add(name); + uplinkValues.add(name); + uplinkBridgeable.add(obj.optBoolean("bridgeable", true)); } - }); - }); - for (int attempt = 0; attempt < 200; attempt++) { - int b = 180 + random.nextInt(10); // 180-189 - int c = random.nextInt(256); // 0-255 - var addr = new IPv4Address(10, b, c, 1); - var candidate = new IPv4Network(addr, 24); - boolean conflicts = false; - for (var ex : existing) { - if (candidate.overlaps(ex)) { - conflicts = true; - break; + } + // keep a saved literal device that isn't currently present selectable + if (selectedUplink != null && !selectedUplink.isEmpty() + && !uplinkValues.contains(selectedUplink)) { + uplinkLabels.add(getString(R.string.network_edit_uplink_identifier, + selectedUplink, getString(R.string.network_edit_uplink_unavailable))); + uplinkValues.add(selectedUplink); + uplinkBridgeable.add(true); + } + updateUplinkDropdown(); + } + + private void addIdentifier( + @NonNull String id, @NonNull String display, boolean bridgeable, + @Nullable org.json.JSONObject data + ) { + String resolved = ""; + boolean br = bridgeable; + if (data != null) { + var ids = data.optJSONArray("identifiers"); + if (ids != null) + for (int i = 0; i < ids.length(); i++) { + var obj = ids.optJSONObject(i); + if (obj != null && id.equals(obj.optString("id"))) { + resolved = obj.optString("name", ""); + br = obj.optBoolean("bridgeable", bridgeable); + break; + } } - } - if (!conflicts) return candidate; } - return new IPv4Network(new IPv4Address(10, 180, 0, 1), 24); + var shown = resolved.isEmpty() + ? getString(R.string.network_edit_uplink_unavailable) : resolved; + uplinkLabels.add(getString(R.string.network_edit_uplink_identifier, display, shown)); + uplinkValues.add(id); + uplinkBridgeable.add(br); + } + + private void updateUplinkDropdown() { + ddL2Uplink.setAdapter(IconItemAdapter.create( + this, uplinkLabels.toArray(new String[0]), R.drawable.ic_ethernet)); + if (uplinkLabels.isEmpty()) return; + int idx = uplinkValues.indexOf(selectedUplink); + if (idx < 0) idx = 0; + selectedUplink = uplinkValues.get(idx); + ddL2Uplink.setText(uplinkLabels.get(idx)); + applyPseudoLock(uplinkBridgeable.get(idx)); + } + + private void onUplinkSelected(int pos) { + if (pos < 0 || pos >= uplinkValues.size()) return; + selectedUplink = uplinkValues.get(pos); + boolean bridgeable = uplinkBridgeable.get(pos); + // re-evaluate the toggle once on selection: bridgeable defaults on + if (bridgeable) swPseudoBridge.setChecked(true); + applyPseudoLock(bridgeable); + } + + /** + * Sets the pseudo-bridge toggle's locked state for an uplink: a + * non-bridgeable uplink (Wi-Fi STA / non-ethernet) forces it on and locks + * it; a bridgeable one leaves it user-controllable. + */ + private void applyPseudoLock(boolean bridgeable) { + if (!bridgeable) swPseudoBridge.setChecked(true); + swPseudoBridge.setSwitchEnabled(bridgeable); + updatePseudoHint(); } - private void updateDhcpFromIPv4() { - if (ipv4Addresses.isEmpty()) return; - var cidr = ipv4Addresses.get(0); - if (cidr == null) return; - if (!dhcpStartManuallyEdited) { - var start = cidr.dhcpPoolStart(); - if (start != null) setTextProgrammatic(inputDhcpStart, start.toString()); + private void updatePseudoHint() { + if (uplinkMode != UplinkMode.L2) return; + int idx = uplinkValues.indexOf(selectedUplink); + boolean bridgeable = idx >= 0 && idx < uplinkBridgeable.size() + ? uplinkBridgeable.get(idx) : true; + tvPseudoHint.setText(getString(bridgeable + ? R.string.network_edit_pseudo_hint_wired + : R.string.network_edit_pseudo_hint_wifi)); + } + + private void generateDefaults() { + bridgeType = BridgeType.LINUX; + uplinkMode = UplinkMode.L3; + ddUplinkMode.setText(labelOfMode(uplinkMode)); + inputMac.setText(generateRandomMac()); + vlans.add(newVlan(0)); + } + + /** A new VLAN entry with paired random networks (empty when exhausted). */ + @NonNull + private VlanConfig newVlan(int vlanId) { + var vlan = VlanConfig.createDefault(vlanId); + var pair = generatePairedCidrs(); + if (pair != null) { + vlan.ipv4().set("cidr", pair[0]); + vlan.ipv6().set("cidr", pair[1]); } - if (!dhcpEndManuallyEdited) { - var end = cidr.dhcpPoolEnd(); - if (end != null) setTextProgrammatic(inputDhcpEnd, end.toString()); + var ipv6 = vlan.ipv6(); + if (bridgeType == BridgeType.GVISOR) { + // gVisor has IPv6 SNAT, so the ULA prefix is routable: default on + ipv6.set("snat", true); + } else { + // a Linux bridge has no IPv6 NAT and Android rarely holds a + // routed prefix, so serving the ULA via DHCPv6/SLAAC hands VMs + // addresses with no connectivity: default to a static ULA CIDR + // with serving off, and pre-fill the Wi-Fi PD uplink for when the + // user switches the source to DHCP-PD + ipv6.set("snat", false); + ipv6.set("source", Ipv6Source.STATIC.key()); + var pd = DataItem.newObject(); + pd.set("uplink", UplinkResolver.ID_WIFI); + ipv6.set("pd", pd); + ipv6.get("dhcp").set("enabled", false); + ipv6.get("slaac").set("enabled", false); } + return vlan; } - private void propagateMacToBridge() { - if (bridgeManuallyEdited) return; - var mac = inputMac.getText().trim(); - if (mac.matches("([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}")) - setTextProgrammatic(inputBridgeName, bridgeNameFromMac(mac)); + /** + * Picks N in 50-250 so that 192.168.N.1/24 and fd00:N::1/64 are both + * free of overlaps with every other network and this network's other + * VLANs. Returns null when no N fits. + */ + @Nullable + private String[] generatePairedCidrs() { + var used4 = new ArrayList(); + var used6 = new ArrayList(); + collectUsedNetworks(used4, used6); + for (int attempt = 0; attempt < 400; attempt++) { + int n = 50 + random.nextInt(201); // 50-250 + IPv4Network cand4; + IPv6Network cand6; + try { + cand4 = IPv4Network.parse(fmt("192.168.%d.1/24", n)); + cand6 = IPv6Network.parse(fmt("fd00:%d::1/64", n)); + } catch (Exception e) { + continue; + } + boolean conflicts = false; + for (var ex : used4) + if (cand4.overlaps(ex)) { + conflicts = true; + break; + } + if (!conflicts) for (var ex : used6) + if (cand6.overlaps(ex)) { + conflicts = true; + break; + } + if (!conflicts) return new String[]{cand4.toString(), cand6.toString()}; + } + return null; } - private void setTextProgrammatic(@NonNull TextInputRowWidget w, @NonNull String text) { - programmaticChange = true; - w.setText(text); - programmaticChange = false; + /** Subnets in use by other networks and by this network's VLAN cards. */ + private void collectUsedNetworks( + @NonNull List out4, @NonNull List out6 + ) { + storeAllBinders(); + var sources = new ArrayList(vlans); + store.forEach((id, cfg) -> { + if (id.equals(editNetworkId)) return; + sources.addAll(cfg.getVlans()); + }); + for (var vlan : sources) { + var net4 = vlan.getIpv4Network(); + if (net4 != null) out4.add(net4); + for (var cidr : vlan.getIpv4Secondary()) { + try { + out4.add(IPv4Network.parse(cidr)); + } catch (Exception ignored) { + } + } + var net6 = vlan.getIpv6Network(); + if (net6 != null) out6.add(net6); + for (var cidr : vlan.getIpv6Secondary()) { + try { + out6.add(IPv6Network.parse(cidr)); + } catch (Exception ignored) { + } + } + } } private void loadExistingConfig() { @@ -268,247 +417,135 @@ private void loadExistingConfig() { finish(); return; } - programmaticChange = true; inputName.setText(config.getName()); - inputBridgeName.setText(config.item.optString("bridge_name", "")); - inputMac.setText(config.item.optString("mac_address", "")); - programmaticChange = false; - swAutoUp.setChecked(config.item.optBoolean("auto_up", false)); - swStp.setChecked(config.item.optBoolean("stp", false)); - swNat.setChecked(config.item.optBoolean("nat", false)); - swDhcp.setChecked(config.item.optBoolean("dhcp_enabled", false)); - programmaticChange = true; - var start = config.item.optString("dhcp_range_start", ""); - var end = config.item.optString("dhcp_range_end", ""); - inputDhcpStart.setText(start); - inputDhcpEnd.setText(end); - programmaticChange = false; - ipv4Addresses.clear(); - parseIPv4Addresses(config, ipv4Addresses); - ipv6Addresses.clear(); - parseIPv6Addresses(config, ipv6Addresses); - dnsServers.clear(); - parseDnsServers(config, dnsServers); - if (dnsServers.isEmpty()) addDefaultDnsServers(); - macManuallyEdited = true; - bridgeManuallyEdited = true; - ipv4ManuallyEdited = true; - dhcpStartManuallyEdited = true; - dhcpEndManuallyEdited = true; + swAutoUp.setChecked(config.isAutoUp()); + swStp.setChecked(config.isStp()); + bridgeType = config.getBridgeType(); + uplinkMode = config.getUplinkMode(); + existingBridgeName = config.getBridgeName(); + ddUplinkMode.setText(labelOfMode(uplinkMode)); + var l2Uplink = config.getL2Uplink(); + if (l2Uplink != null && !l2Uplink.isEmpty()) selectedUplink = l2Uplink; + swPseudoBridge.setChecked(config.isL2PseudoBridge()); + rebuildUplinks(null); // reflect the loaded selection; refreshed by IPC + var l3Mac = config.l3().optString("mac_address", ""); + inputMac.setText(l3Mac == null || l3Mac.isEmpty() ? generateRandomMac() : l3Mac); + vlans.clear(); + for (var vlan : config.getVlans()) { + // deep copy so cancel doesn't mutate the store + var copy = DataItem.newObject(); + copy.puts(vlan.item); + vlans.add(new VlanConfig(copy)); + } } - private void onAddIPv4() { - var addr = inputIPv4.getText().trim(); - if (addr.isEmpty()) return; - if (!IPv4Network.isValid(addr)) { - inputIPv4.setError(getString(R.string.network_edit_error_address_invalid)); - return; - } - IPv4Network ip; - try { - ip = IPv4Network.parse(addr); - } catch (Exception ignored) { - inputIPv4.setError(getString(R.string.network_edit_error_address_invalid)); - return; - } - inputIPv4.setError(null); - ipv4Addresses.add(ip); - ipv4ManuallyEdited = true; - inputIPv4.setText(""); - buildAddressList(layoutIPv4, tvIPv4Empty, ipv4Addresses, this::updateDhcpFromIPv4); - updateDhcpFromIPv4(); + /** Editing a RUNNING network is not allowed; show a banner and block save. */ + private void checkRunningState() { + DaemonConnection.getInstance().buildRequest("network_status") + .put("network_id", editNetworkId.toString()) + .onResponse(resp -> { + var state = resp.optString("state", ""); + if (state.equalsIgnoreCase("running") + || state.equalsIgnoreCase("starting")) { + mainHandler.post(() -> { + tvRunningBanner.setVisibility(VISIBLE); + fab.setEnabled(false); + }); + } + }) + .onUnsuccessful(r -> { + }) + .onError(e -> { + }) + .invoke(); } - private void onAddIPv6() { - var addr = inputIPv6.getText().trim(); - if (addr.isEmpty()) return; - if (!IPv6Network.isValid(addr)) { - inputIPv6.setError(getString(R.string.network_edit_error_address_invalid)); - return; - } - IPv6Network ip; - try { - ip = IPv6Network.parse(addr); - } catch (Exception ignored) { - inputIPv6.setError(getString(R.string.network_edit_error_address_invalid)); - return; - } - inputIPv6.setError(null); - ipv6Addresses.add(ip); - inputIPv6.setText(""); - buildAddressList(layoutIPv6, tvIPv6Empty, ipv6Addresses, null); + private void onAddVlan() { + storeAllBinders(); + // smallest free id in 0, 10, 20, ... + var used = new java.util.HashSet(); + for (var vlan : vlans) used.add(vlan.getVlanId()); + int id = 0; + while (used.contains(id) && id < 4090) id += 10; + vlans.add(newVlan(id)); + rebuildVlanCards(); } - private void onAddDns() { - var addr = inputDns.getText().trim(); - if (addr.isEmpty()) return; - if (!IPv4Address.isValid(addr)) { - inputDns.setError(getString(R.string.network_edit_error_dns_invalid)); - return; - } - var ip = IPv4Address.parse(addr); - if (ip == null) { - inputDns.setError(getString(R.string.network_edit_error_dns_invalid)); - return; - } - for (var existing : dnsServers) { - if (existing.value() == ip.value()) { - inputDns.setText(""); - return; - } + private void rebuildVlanCards() { + storeAllBinders(); + containerVlans.removeAllViews(); + binders.clear(); + tvVlanEmpty.setVisibility(vlans.isEmpty() ? VISIBLE : GONE); + var inflater = LayoutInflater.from(this); + for (int i = 0; i < vlans.size(); i++) { + final int idx = i; + var view = inflater.inflate(R.layout.item_network_vlan, containerVlans, false); + var binder = new VlanCardBinder(view, uplinkValues); + binder.bind(vlans.get(i), bridgeType); + binder.delete.setOnClickListener(v -> { + vlans.remove(idx); + rebuildVlanCards(); + }); + binders.add(binder); + containerVlans.addView(view); } - inputDns.setError(null); - dnsServers.add(ip); - inputDns.setText(""); - buildAddressList(layoutDns, tvDnsEmpty, dnsServers, null); } - private void buildAddressList( - @NonNull LinearLayout container, - @NonNull TextView emptyView, - @NonNull List list, - @Nullable Runnable onRemoved - ) { - container.removeAllViews(); - if (list.isEmpty()) { - emptyView.setVisibility(VISIBLE); - } else { - emptyView.setVisibility(GONE); - for (int i = 0; i < list.size(); i++) { - final int idx = i; - var row = new LinearLayout(this); - row.setOrientation(LinearLayout.HORIZONTAL); - row.setGravity(Gravity.CENTER_VERTICAL); - int pad = (int) (4 * getResources().getDisplayMetrics().density); - row.setPadding(0, pad, 0, pad); - var tv = new TextView(this); - tv.setText(list.get(i).toString()); - tv.setTextSize(15); - var lp = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f); - tv.setLayoutParams(lp); - row.addView(tv); - var btn = new ImageButton(this); - btn.setImageResource(R.drawable.ic_delete); - btn.setBackground(null); - btn.setContentDescription(getString(R.string.network_edit_remove_address)); - btn.setOnClickListener(v -> { - list.remove(idx); - buildAddressList(container, emptyView, list, onRemoved); - if (onRemoved != null) onRemoved.run(); - }); - row.addView(btn); - container.addView(row); - } - } + private void storeAllBinders() { + for (int i = 0; i < binders.size() && i < vlans.size(); i++) + binders.get(i).store(vlans.get(i)); } private void onSaveClicked() { - inputName.setError(null); - inputBridgeName.setError(null); - inputMac.setError(null); - inputIPv4.setError(null); - inputIPv6.setError(null); - inputDns.setError(null); - inputDhcpStart.setError(null); - inputDhcpEnd.setError(null); - if (!inputIPv4.getText().trim().isEmpty()) { - inputIPv4.setError(getString(R.string.network_edit_error_ipv4_not_added)); - return; - } - if (!inputIPv6.getText().trim().isEmpty()) { - inputIPv6.setError(getString(R.string.network_edit_error_ipv6_not_added)); - return; - } - if (!inputDns.getText().trim().isEmpty()) { - inputDns.setError(getString(R.string.network_edit_error_dns_not_added)); - return; - } - if (dnsServers.isEmpty()) { - inputDns.setError(getString(R.string.network_edit_error_dns_empty)); - return; - } + storeAllBinders(); var name = inputName.getText().trim(); - var bridgeName = inputBridgeName.getText().trim(); - var mac = inputMac.getText().trim(); if (name.isEmpty()) { inputName.setError(getString(R.string.network_edit_error_name_empty)); return; } - if (bridgeName.isEmpty()) { - inputBridgeName.setError(getString(R.string.network_edit_error_bridge_empty)); - return; - } - if (!bridgeName.matches("[a-zA-Z][a-zA-Z0-9_-]*")) { - inputBridgeName.setError(getString(R.string.network_edit_error_bridge_invalid)); - return; - } - if (!mac.isEmpty() && !mac.matches("([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}")) { - inputMac.setError(getString(R.string.network_edit_error_mac_invalid)); - return; - } + inputName.setError(null); if (!store.isNameUnique(name, editNetworkId)) { inputName.setError(getString(R.string.network_edit_error_name_duplicate)); return; } - if (!isBridgeUnique(bridgeName)) { - inputBridgeName.setError(getString(R.string.network_edit_error_bridge_duplicate)); - return; - } - if (!mac.isEmpty() && !isMacUnique(mac)) { - inputMac.setError(getString(R.string.network_edit_error_mac_duplicate)); - return; - } - var ipv4Conflict = checkIPv4Overlap(); - if (ipv4Conflict != null) { - Toast.makeText(this, ipv4Conflict, LENGTH_LONG).show(); - return; - } - var ipv6Conflict = checkIPv6Overlap(); - if (ipv6Conflict != null) { - Toast.makeText(this, ipv6Conflict, LENGTH_LONG).show(); - return; - } - if (swDhcp.isChecked()) { - var dhcpStart = inputDhcpStart.getText().trim(); - var dhcpEnd = inputDhcpEnd.getText().trim(); - if (!dhcpStart.isEmpty() && !IPv4Address.isValid(dhcpStart)) { - inputDhcpStart.setError(getString(R.string.network_edit_error_dhcp_start_invalid)); - return; - } - if (!dhcpEnd.isEmpty() && !IPv4Address.isValid(dhcpEnd)) { - inputDhcpEnd.setError(getString(R.string.network_edit_error_dhcp_end_invalid)); - return; - } - if (!dhcpStart.isEmpty() && !dhcpEnd.isEmpty()) { - long start = requireNonNull(IPv4Address.parse(dhcpStart)).value(); - long end = requireNonNull(IPv4Address.parse(dhcpEnd)).value(); - if (start >= end) { - inputDhcpStart.setError(getString(R.string.network_edit_error_dhcp_invalid)); - return; - } - } - } var config = new NetworkConfig(); if (editMode && editNetworkId != null) config.setId(editNetworkId); config.setName(name); - config.item.set("bridge_name", bridgeName); - config.item.set("mac_address", mac); + config.setBridgeName(existingBridgeName != null + ? existingBridgeName + : fmt("br%s", config.getId().toString().substring(0, 8).replace("-", ""))); config.item.set("auto_up", swAutoUp.isChecked()); config.item.set("stp", swStp.isChecked()); - config.item.set("nat", swNat.isChecked()); - config.item.set("dhcp_enabled", swDhcp.isChecked()); - setIPv4Addresses(config, ipv4Addresses); - setIPv6Addresses(config, ipv6Addresses); - setDnsServers(config, dnsServers); - config.item.set("dhcp_range_start", inputDhcpStart.getText().trim()); - config.item.set("dhcp_range_end", inputDhcpEnd.getText().trim()); + config.setBridgeType(bridgeType); + config.setUplinkMode(uplinkMode); + if (uplinkMode == UplinkMode.L2) { + config.l2().set("uplink", selectedUplink); + config.l2().set("pseudo_bridge", swPseudoBridge.isChecked()); + } else { + config.l3().set("mac_address", inputMac.getText().trim()); + var arr = DataItem.newArray(); + for (var vlan : vlans) arr.append(vlan.item); + config.l3().set("vlans", arr); + } + try { + NetworkConfigValidator.validate(config); + } catch (IllegalArgumentException e) { + Toast.makeText(this, e.getMessage(), LENGTH_LONG).show(); + return; + } + var overlap = checkOverlaps(config); + if (overlap != null) { + Toast.makeText(this, overlap, LENGTH_LONG).show(); + return; + } if (editMode) { store.update(config); } else { store.add(config); } store.save(this); + syncToDaemon(config); Toast.makeText(this, editMode ? getString(R.string.network_edit_saved, name) : getString(R.string.network_create_success, name), @@ -516,118 +553,86 @@ private void onSaveClicked() { finish(); } - private boolean isBridgeUnique(@NonNull String bridge) { - final boolean[] unique = {true}; - store.forEach((id, cfg) -> { - if (id.equals(editNetworkId)) return; - if (bridge.equals(cfg.item.optString("bridge_name", ""))) unique[0] = false; - }); - return unique[0]; - } - - private boolean isMacUnique(@NonNull String mac) { - final boolean[] unique = {true}; - var macLower = mac.toLowerCase(Locale.ROOT); - store.forEach((id, cfg) -> { - if (id.equals(editNetworkId)) return; - if (macLower.equals(cfg.item.optString("mac_address", "").toLowerCase(Locale.ROOT))) - unique[0] = false; - }); - return unique[0]; - } - - @Nullable - private String checkIPv4Overlap() { - for (var myCidr : ipv4Addresses) { - var result = new String[1]; - store.forEach((id, cfg) -> { - if (result[0] != null) return; - if (id.equals(editNetworkId)) return; - var others = new ArrayList(); - parseIPv4Addresses(cfg, others); - for (var other : others) { - if (!myCidr.overlaps(other)) continue; - result[0] = getString( - R.string.network_edit_error_ipv4_overlap, - myCidr.toString(), cfg.getName(), other - ); - return; - } - }); - if (result[0] != null) return result[0]; - } - return null; + /** Push the modified config to the daemon if it already knows the network. */ + private void syncToDaemon(@NonNull NetworkConfig config) { + var conn = DaemonConnection.getInstance(); + conn.buildRequest("network_exists") + .put("network_id", config.getId()) + .onResponse(resp -> { + if (!resp.optBoolean("exists", false)) return; + conn.buildRequest("network_modify") + .put("config", config) + .onUnsuccessful(r -> { + }) + .onError(e -> { + }) + .invoke(); + }) + .onUnsuccessful(r -> { + }) + .onError(e -> { + }) + .invoke(); } @Nullable - private String checkIPv6Overlap() { - for (var myCidr : ipv6Addresses) { - var result = new String[1]; - store.forEach((id, cfg) -> { - if (result[0] != null) return; - if (id.equals(editNetworkId)) return; - var others = new ArrayList(); - parseIPv6Addresses(cfg, others); - for (var other : others) { - if (!myCidr.overlaps(other)) continue; - result[0] = getString( - R.string.network_edit_error_ipv6_overlap, - myCidr.toString(), cfg.getName(), other - ); - return; + private String checkOverlaps(@NonNull NetworkConfig config) { + var myV4 = new ArrayList(); + var myV6 = new ArrayList(); + for (var vlan : config.getVlans()) { + var net4 = vlan.getIpv4Network(); + if (net4 != null) myV4.add(net4); + for (var cidr : vlan.getIpv4Secondary()) { + try { + myV4.add(IPv4Network.parse(cidr)); + } catch (Exception ignored) { } - }); - if (result[0] != null) return result[0]; - } - return null; - } - - private static void parseIPv4Addresses(@NonNull NetworkConfig cfg, @NonNull List out) { - cfg.item.get("ipv4_addresses").forEachArray(a -> { - try { - out.add(IPv4Network.parse(a.asString())); - } catch (Exception ignored) { } - }); - } - - private static void parseIPv6Addresses(@NonNull NetworkConfig cfg, @NonNull List out) { - cfg.item.get("ipv6_addresses").forEachArray(a -> { - try { - out.add(IPv6Network.parse(a.asString())); - } catch (Exception ignored) { + var net6 = vlan.getIpv6Network(); + if (net6 != null) myV6.add(net6); + for (var cidr : vlan.getIpv6Secondary()) { + try { + myV6.add(IPv6Network.parse(cidr)); + } catch (Exception ignored) { + } } - }); - } - - private static void parseDnsServers(@NonNull NetworkConfig cfg, @NonNull List out) { - cfg.item.get("dns_servers").forEachArray(a -> { - try { - var ip = IPv4Address.parse(a.asString()); - if (ip != null) out.add(ip); - } catch (Exception ignored) { + } + // overlaps within this network + for (int i = 0; i < myV4.size(); i++) + for (int j = i + 1; j < myV4.size(); j++) + if (myV4.get(i).overlaps(myV4.get(j))) + return getString(R.string.network_edit_error_self_overlap, + myV4.get(i).toString(), myV4.get(j).toString()); + for (int i = 0; i < myV6.size(); i++) + for (int j = i + 1; j < myV6.size(); j++) + if (myV6.get(i).overlaps(myV6.get(j))) + return getString(R.string.network_edit_error_self_overlap, + myV6.get(i).toString(), myV6.get(j).toString()); + // overlaps against other networks + var result = new String[1]; + store.forEach((id, other) -> { + if (result[0] != null || id.equals(editNetworkId)) return; + for (var vlan : other.getVlans()) { + var otherNet = vlan.getIpv4Network(); + if (otherNet != null) { + for (var mine : myV4) { + if (!mine.overlaps(otherNet)) continue; + result[0] = getString(R.string.network_edit_error_ipv4_overlap, + mine.toString(), other.getName(), otherNet); + return; + } + } + var otherNet6 = vlan.getIpv6Network(); + if (otherNet6 != null) { + for (var mine : myV6) { + if (!mine.overlaps(otherNet6)) continue; + result[0] = getString(R.string.network_edit_error_ipv6_overlap, + mine.toString(), other.getName(), otherNet6); + return; + } + } } }); - } - - private static void setIPv4Addresses(@NonNull NetworkConfig cfg, @NonNull List addresses) { - var arr = DataItem.newArray(); - for (var addr : addresses) - arr.append(DataItem.newString(addr.toString())); - cfg.item.set("ipv4_addresses", arr); - } - - private static void setIPv6Addresses(@NonNull NetworkConfig cfg, @NonNull List addresses) { - var arr = DataItem.newArray(); - for (var addr : addresses) - arr.append(DataItem.newString(addr.toString())); - cfg.item.set("ipv6_addresses", arr); - } - - private static void setDnsServers(@NonNull NetworkConfig cfg, @NonNull List dns) { - var arr = DataItem.newArray(); - for (var ip : dns) - arr.append(DataItem.newString(ip.toString())); - cfg.item.set("dns_servers", arr); + return result[0]; } } diff --git a/app/src/main/java/cn/classfun/droidvm/ui/network/edit/VlanCardBinder.java b/app/src/main/java/cn/classfun/droidvm/ui/network/edit/VlanCardBinder.java new file mode 100644 index 000000000..f134f8048 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/ui/network/edit/VlanCardBinder.java @@ -0,0 +1,237 @@ +package cn.classfun.droidvm.ui.network.edit; + +import static android.view.View.GONE; +import static android.view.View.VISIBLE; +import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; + +import android.view.View; +import android.widget.ArrayAdapter; +import android.widget.AutoCompleteTextView; +import android.widget.ImageButton; +import android.widget.TextView; + +import androidx.annotation.NonNull; + +import com.google.android.material.materialswitch.MaterialSwitch; +import com.google.android.material.textfield.TextInputEditText; + +import java.util.ArrayList; +import java.util.List; + +import cn.classfun.droidvm.R; +import cn.classfun.droidvm.daemon.network.backend.UplinkResolver; +import cn.classfun.droidvm.lib.store.base.DataItem; +import cn.classfun.droidvm.lib.store.network.BridgeType; +import cn.classfun.droidvm.lib.store.network.Ipv6Source; +import cn.classfun.droidvm.lib.store.network.VlanConfig; + +/** + * Binds one VLAN card view to a VlanConfig: populates the inputs from + * the config and reads them back into it on save. + */ +final class VlanCardBinder { + final View view; + private final TextView title; + private final TextInputEditText vlanId; + final ImageButton delete; + private final TextInputEditText v4Cidr; + private final MaterialSwitch v4Snat; + private final MaterialSwitch v4Dhcp; + private final View groupV4Dhcp; + private final TextInputEditText v4OffsetStart; + private final TextInputEditText v4OffsetEnd; + private final AutoCompleteTextView v6Source; + private final View tilV6Cidr; + private final TextInputEditText v6Cidr; + private final View groupV6Pd; + private final AutoCompleteTextView pdUplink; + private final TextInputEditText pdDuid; + private final MaterialSwitch v6Snat; + private final MaterialSwitch v6Dhcp; + private final View groupV6Dhcp; + private final TextInputEditText v6OffsetStart; + private final TextInputEditText v6OffsetEnd; + private final MaterialSwitch v6Slaac; + private final View tilV4Secondary; + private final TextInputEditText v4Secondary; + private final View tilV6Secondary; + private final TextInputEditText v6Secondary; + private final TextInputEditText dns; + private final String[] sourceLabels; + + VlanCardBinder(@NonNull View view, @NonNull List uplinkNames) { + this.view = view; + title = view.findViewById(R.id.tv_vlan_title); + vlanId = view.findViewById(R.id.et_vlan_id); + delete = view.findViewById(R.id.btn_vlan_delete); + v4Cidr = view.findViewById(R.id.et_v4_cidr); + v4Snat = view.findViewById(R.id.sw_v4_snat); + v4Dhcp = view.findViewById(R.id.sw_v4_dhcp); + groupV4Dhcp = view.findViewById(R.id.group_v4_dhcp); + v4OffsetStart = view.findViewById(R.id.et_v4_offset_start); + v4OffsetEnd = view.findViewById(R.id.et_v4_offset_end); + v6Source = view.findViewById(R.id.dd_v6_source); + tilV6Cidr = view.findViewById(R.id.til_v6_cidr); + v6Cidr = view.findViewById(R.id.et_v6_cidr); + groupV6Pd = view.findViewById(R.id.group_v6_pd); + pdUplink = view.findViewById(R.id.dd_pd_uplink); + pdDuid = view.findViewById(R.id.et_pd_duid); + v6Snat = view.findViewById(R.id.sw_v6_snat); + v6Dhcp = view.findViewById(R.id.sw_v6_dhcp); + groupV6Dhcp = view.findViewById(R.id.group_v6_dhcp); + v6OffsetStart = view.findViewById(R.id.et_v6_offset_start); + v6OffsetEnd = view.findViewById(R.id.et_v6_offset_end); + v6Slaac = view.findViewById(R.id.sw_v6_slaac); + tilV4Secondary = view.findViewById(R.id.til_v4_secondary); + v4Secondary = view.findViewById(R.id.et_v4_secondary); + tilV6Secondary = view.findViewById(R.id.til_v6_secondary); + v6Secondary = view.findViewById(R.id.et_v6_secondary); + dns = view.findViewById(R.id.et_dns); + var ctx = view.getContext(); + sourceLabels = new String[]{ + ctx.getString(R.string.network_edit_vlan_v6_static), + ctx.getString(R.string.network_edit_vlan_v6_pd), + }; + v6Source.setAdapter(new ArrayAdapter<>( + ctx, android.R.layout.simple_list_item_1, sourceLabels)); + v6Source.setOnItemClickListener((p, v, pos, id) -> updateV6SourceViews()); + pdUplink.setAdapter(new ArrayAdapter<>( + ctx, android.R.layout.simple_list_item_1, uplinkNames)); + v4Dhcp.setOnCheckedChangeListener((b, checked) -> + groupV4Dhcp.setVisibility(checked ? VISIBLE : GONE)); + v6Dhcp.setOnCheckedChangeListener((b, checked) -> + groupV6Dhcp.setVisibility(checked ? VISIBLE : GONE)); + } + + void bind(@NonNull VlanConfig vlan, @NonNull BridgeType type) { + var ctx = view.getContext(); + int id = vlan.getVlanId(); + title.setText(id == 0 + ? ctx.getString(R.string.network_edit_vlan_untagged) + : ctx.getString(R.string.network_edit_vlan_title)); + vlanId.setText(String.valueOf(id)); + var net4 = vlan.getIpv4Cidr(); + v4Cidr.setText(net4 != null ? net4 : ""); + v4Snat.setChecked(vlan.isIpv4Snat()); + v4Dhcp.setChecked( + vlan.ipv4().opt("dhcp", DataItem.newObject()).optBoolean("enabled", false)); + groupV4Dhcp.setVisibility(v4Dhcp.isChecked() ? VISIBLE : GONE); + v4OffsetStart.setText(String.valueOf(vlan.getDhcp4OffsetStart())); + v4OffsetEnd.setText(String.valueOf(vlan.getDhcp4OffsetEnd())); + var source = vlan.getIpv6Source(); + v6Source.setText( + source == Ipv6Source.DHCP_PD ? sourceLabels[1] : sourceLabels[0], false); + var cidr6 = vlan.getIpv6Cidr(); + v6Cidr.setText(cidr6 != null ? cidr6 : ""); + var uplink = vlan.getPdUplink(); + pdUplink.setText(uplink == null || uplink.isEmpty() + ? UplinkResolver.ID_WIFI : uplink, false); + var duid = vlan.getPdDuid(); + pdDuid.setText(duid != null ? duid : ""); + v6Snat.setChecked(vlan.isIpv6Snat()); + v6Dhcp.setChecked( + vlan.ipv6().opt("dhcp", DataItem.newObject()).optBoolean("enabled", false)); + groupV6Dhcp.setVisibility(v6Dhcp.isChecked() ? VISIBLE : GONE); + v6OffsetStart.setText(String.valueOf(vlan.getDhcp6OffsetStart())); + v6OffsetEnd.setText(String.valueOf(vlan.getDhcp6OffsetEnd())); + v6Slaac.setChecked( + vlan.ipv6().opt("slaac", DataItem.newObject()).optBoolean("enabled", false)); + v4Secondary.setText(String.join(", ", vlan.getIpv4Secondary())); + v6Secondary.setText(String.join(", ", vlan.getIpv6Secondary())); + dns.setText(String.join(", ", vlan.getDnsServers())); + applyBridgeType(type); + updateV6SourceViews(); + } + + /** gVisor: no DHCP-PD, no secondary networks, IPv6 SNAT allowed. */ + void applyBridgeType(@NonNull BridgeType type) { + boolean gvisor = type == BridgeType.GVISOR; + // a Linux bridge can't do IPv6 SNAT, but keep the stored value and + // only gray the switch out, so flipping to Linux and back to gVisor + // doesn't silently lose an enabled SNAT + v6Snat.setEnabled(gvisor); + tilV4Secondary.setVisibility(gvisor ? GONE : VISIBLE); + tilV6Secondary.setVisibility(gvisor ? GONE : VISIBLE); + if (gvisor && isPdSelected()) v6Source.setText(sourceLabels[0], false); + var ctx = view.getContext(); + var labels = gvisor + ? new String[]{sourceLabels[0]} + : sourceLabels; + v6Source.setAdapter(new ArrayAdapter<>( + ctx, android.R.layout.simple_list_item_1, labels)); + updateV6SourceViews(); + } + + private boolean isPdSelected() { + return v6Source.getText().toString().equals(sourceLabels[1]); + } + + private void updateV6SourceViews() { + boolean pd = isPdSelected(); + tilV6Cidr.setVisibility(pd ? GONE : VISIBLE); + groupV6Pd.setVisibility(pd ? VISIBLE : GONE); + } + + /** Reads the views back into the VlanConfig item. */ + void store(@NonNull VlanConfig vlan) { + vlan.item.set("vlan_id", parseLong(textOf(vlanId), 0)); + var ipv4 = vlan.ipv4(); + ipv4.set("cidr", textOf(v4Cidr)); + ipv4.set("snat", v4Snat.isChecked()); + var dhcp4 = DataItem.newObject(); + dhcp4.set("enabled", v4Dhcp.isChecked()); + dhcp4.set("offset_start", parseLong(textOf(v4OffsetStart), 128)); + dhcp4.set("offset_end", parseLong(textOf(v4OffsetEnd), 192)); + ipv4.set("dhcp", dhcp4); + var ipv6 = vlan.ipv6(); + ipv6.set("source", + (isPdSelected() ? Ipv6Source.DHCP_PD : Ipv6Source.STATIC).key()); + ipv6.set("cidr", textOf(v6Cidr)); + var pd = DataItem.newObject(); + pd.set("uplink", pdUplink.getText().toString().trim()); + pd.set("duid", textOf(pdDuid)); + ipv6.set("pd", pd); + ipv6.set("snat", v6Snat.isChecked()); + var dhcp6 = DataItem.newObject(); + dhcp6.set("enabled", v6Dhcp.isChecked()); + dhcp6.set("offset_start", parseLong(textOf(v6OffsetStart), 128)); + dhcp6.set("offset_end", parseLong(textOf(v6OffsetEnd), 192)); + ipv6.set("dhcp", dhcp6); + var slaac = DataItem.newObject(); + slaac.set("enabled", v6Slaac.isChecked()); + ipv6.set("slaac", slaac); + vlan.item.set("ipv4_secondary", splitList(textOf(v4Secondary))); + vlan.item.set("ipv6_secondary", splitList(textOf(v6Secondary))); + vlan.item.set("dns_servers", splitList(textOf(dns))); + } + + @NonNull + private static String textOf(@NonNull TextInputEditText et) { + var text = et.getText(); + return text == null ? "" : text.toString().trim(); + } + + private static long parseLong(@NonNull String s, long def) { + try { + return Long.parseLong(s); + } catch (NumberFormatException e) { + return def; + } + } + + @NonNull + private static DataItem splitList(@NonNull String text) { + var arr = DataItem.newArray(); + for (var part : text.split("[,\\s]+")) { + var trimmed = part.trim(); + if (!trimmed.isEmpty()) arr.append(new DataItem(trimmed)); + } + return arr; + } + + @SuppressWarnings("unused") + @NonNull + static String describe(int vlanId) { + return vlanId == 0 ? "untagged" : fmt("VLAN %d", vlanId); + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/ui/network/info/NetworkInfoActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/network/info/NetworkInfoActivity.java index eabba7d74..77ef9616f 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/network/info/NetworkInfoActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/network/info/NetworkInfoActivity.java @@ -38,6 +38,7 @@ import com.google.android.material.tabs.TabLayout; import org.json.JSONArray; +import org.json.JSONObject; import java.text.DateFormat; import java.text.SimpleDateFormat; @@ -49,10 +50,12 @@ import cn.classfun.droidvm.R; import cn.classfun.droidvm.lib.daemon.DaemonConnection; -import cn.classfun.droidvm.lib.network.IPv4Address; +import cn.classfun.droidvm.lib.store.network.BridgeType; +import cn.classfun.droidvm.lib.store.network.Ipv6Source; import cn.classfun.droidvm.lib.store.network.NetworkConfig; import cn.classfun.droidvm.lib.store.network.NetworkState; import cn.classfun.droidvm.lib.store.network.NetworkStore; +import cn.classfun.droidvm.lib.store.network.UplinkMode; import cn.classfun.droidvm.lib.ui.SwipeableTabActivity; import cn.classfun.droidvm.lib.ui.UIContext; import cn.classfun.droidvm.ui.network.NetworkActions; @@ -81,9 +84,15 @@ public final class NetworkInfoActivity extends SwipeableTabActivity { private MaterialButton btnStartStop, btnEdit, btnDelete; private MaterialToolbar toolbar; private JSONArray liveAddresses = new JSONArray(); + private JSONArray addressEntries = new JSONArray(); + private boolean hasAddressEntries = false; + private JSONArray pdStatus = new JSONArray(); private JSONArray liveInterfaces = new JSONArray(); private JSONArray neighbors = new JSONArray(); private JSONArray dhcpLeases = new JSONArray(); + private JSONObject infoData = new JSONObject(); + /** Signature of the last data rendered into the tabs; skips no-op rebuilds. */ + private String lastRenderedSig = null; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { @@ -170,7 +179,7 @@ private void populateInfo() { } private void setupTabs() { - hasDhcp = config.item.optBoolean("dhcp_enabled", false); + hasDhcp = hasAnyDhcp(); int count = hasDhcp ? 5 : 4; tabContainer.removeAllViews(); tabLayout.clearOnTabSelectedListeners(); @@ -218,7 +227,7 @@ private List getTabTitles() { titles.add(getString(R.string.network_info_tab_basic)); titles.add(getString(R.string.network_info_tab_addresses)); titles.add(getString(R.string.network_info_tab_ports)); - if (config != null && config.item.optBoolean("dhcp_enabled", false)) + if (hasAnyDhcp()) titles.add(getString(R.string.network_info_tab_dhcp)); titles.add(getString(R.string.network_info_tab_neighbors)); return titles; @@ -260,9 +269,15 @@ private void syncState() { mainHandler.post(() -> { if (isFinishing()) return; if (data != null) { + infoData = data; currentState = optEnum(data, "state", NetworkState.STOPPED); liveAddresses = data.optJSONArray("live_addresses"); if (liveAddresses == null) liveAddresses = new JSONArray(); + addressEntries = data.optJSONArray("address_entries"); + hasAddressEntries = addressEntries != null; + if (addressEntries == null) addressEntries = new JSONArray(); + pdStatus = data.optJSONArray("pd"); + if (pdStatus == null) pdStatus = new JSONArray(); liveInterfaces = data.optJSONArray("live_interfaces"); if (liveInterfaces == null) liveInterfaces = new JSONArray(); neighbors = data.optJSONArray("neighbors"); @@ -271,7 +286,15 @@ private void syncState() { if (dhcpLeases == null) dhcpLeases = new JSONArray(); } updateStateUI(); - refreshTabContents(); + // Only rebuild the tab views when the data actually changed; + // re-inflating every 3s poll made the contents visibly flicker. + if (data != null) { + var sig = data.toString(); + if (!sig.equals(lastRenderedSig)) { + lastRenderedSig = sig; + refreshTabContents(); + } + } }); }; DaemonConnection.getInstance() @@ -402,6 +425,52 @@ private String strStatus(boolean stat) { getString(R.string.network_info_disabled); } + /** A clickable helper-tool row (pbridge/gvswitch/bridgedhcp) opening its log. */ + private void addToolRow(@NonNull LinearLayout container, @NonNull String key, boolean running) { + int iconRes; + String label; + switch (key) { + case "pbridge": + iconRes = R.drawable.ic_connection; + label = getString(R.string.network_info_pbridge); + break; + case "gvswitch": + iconRes = R.drawable.ic_switch; + label = getString(R.string.network_info_gvswitch); + break; + case "bridgedhcp": + iconRes = R.drawable.ic_router; + label = getString(R.string.network_info_bridgedhcp); + break; + default: + iconRes = R.drawable.ic_router; + label = key; + break; + } + var view = LayoutInflater.from(this) + .inflate(R.layout.item_network_entry, container, false); + ((ImageView) view.findViewById(R.id.iv_icon)).setImageResource(iconRes); + ((TextView) view.findViewById(R.id.tv_line1)).setText(label); + var line2 = (TextView) view.findViewById(R.id.tv_line2); + line2.setText(fmt("%s · %s", + getString(running ? R.string.network_info_tool_running + : R.string.network_info_tool_stopped), + getString(R.string.network_info_tool_view_log))); + line2.setVisibility(VISIBLE); + var ta = getTheme().obtainStyledAttributes( + new int[]{android.R.attr.selectableItemBackground}); + view.setBackgroundResource(ta.getResourceId(0, 0)); + ta.recycle(); + view.setOnClickListener(v -> { + var intent = new Intent(this, NetworkToolLogActivity.class); + intent.putExtra("network_id", networkId.toString()); + intent.putExtra("tool", key); + intent.putExtra("title", label); + startActivity(intent); + }); + container.addView(view); + } + private void bindBasic(@NonNull LinearLayout container) { if (config == null) return; var header = LayoutInflater.from(this) @@ -418,63 +487,155 @@ private void bindBasic(@NonNull LinearLayout container) { updateStateUI(); addInfoRow(container, R.drawable.ic_switch, getString(R.string.network_info_bridge), config.item.optString("bridge_name", "")); - var mac = config.item.optString("mac_address", ""); + addInfoRow(container, R.drawable.ic_switch, + getString(R.string.network_edit_bridge_type), + getString(config.getBridgeType() == BridgeType.GVISOR + ? R.string.network_edit_bridge_type_gvisor + : R.string.network_edit_bridge_type_linux)); + String modeLabel; + switch (config.getUplinkMode()) { + case L2: + modeLabel = getString(R.string.network_edit_uplink_l2); + break; + case L3: + modeLabel = getString(R.string.network_edit_uplink_l3); + break; + default: + modeLabel = getString(R.string.network_edit_uplink_none); + break; + } + addInfoRow(container, R.drawable.ic_web_plus, + getString(R.string.network_edit_uplink_mode), modeLabel); + var mac = config.getBridgeMacAddress(); addInfoRow(container, R.drawable.ic_ethernet, getString(R.string.network_info_mac), - mac.isEmpty() ? "-" : mac); + mac == null ? "-" : mac); addInfoRow(container, R.drawable.ic_refresh_auto, getString(R.string.network_info_auto_up), strStatus(config.item.optBoolean("auto_up", false))); addInfoRow(container, R.drawable.ic_file_tree, getString(R.string.network_info_stp), strStatus(config.item.optBoolean("stp", false))); - addInfoRow(container, R.drawable.ic_web_plus, - getString(R.string.network_info_nat), - strStatus(config.item.optBoolean("nat", false))); - addInfoRow(container, R.drawable.ic_router, - getString(R.string.network_info_dhcp), - strStatus(config.item.optBoolean("dhcp_enabled", false))); - if (config.item.optBoolean("dhcp_enabled", false)) try { - var startStr = config.item.optString("dhcp_range_start", ""); - var endStr = config.item.optString("dhcp_range_end", ""); - if (!startStr.isEmpty() && !endStr.isEmpty()) { - var start = requireNonNull(IPv4Address.parse(startStr)); - var end = requireNonNull(IPv4Address.parse(endStr)); - long count = end.value() - start.value() + 1; - if (count < 0) count = 0; - var range = getResources().getQuantityString( - R.plurals.network_info_dhcp_pool, (int) count, - start.toString(), end.toString(), count - ); + if (config.getUplinkMode() == UplinkMode.L2) { + var uplink = config.getL2Uplink(); + var resolved = infoData.optString("resolved_uplink", ""); + addInfoRow(container, R.drawable.ic_ethernet, + getString(R.string.network_edit_l2_uplink), + resolved.isEmpty() ? (uplink == null ? "-" : uplink) + : fmt("%s (%s)", uplink, resolved)); + } + for (var vlan : config.getVlans()) { + var summary = new StringBuilder(); + var net4 = vlan.getIpv4Cidr(); + if (net4 != null) { + summary.append(net4); + if (vlan.isIpv4Snat()) summary.append(" SNAT"); + if (vlan.isDhcp4Enabled()) summary.append(fmt( + " DHCP %d-%d", + vlan.getDhcp4OffsetStart(), vlan.getDhcp4OffsetEnd())); + } + if (vlan.getIpv6Source() == Ipv6Source.DHCP_PD) { + if (summary.length() > 0) summary.append("\n"); + summary.append("IPv6: DHCP-PD via ").append(vlan.getPdUplink()); + } else if (vlan.getIpv6Cidr() != null) { + if (summary.length() > 0) summary.append("\n"); + summary.append(vlan.getIpv6Cidr()); + // IPv6 SNAT only takes effect on gVisor bridges + if (vlan.isIpv6Snat() && config.getBridgeType() == BridgeType.GVISOR) + summary.append(" SNAT"); + if (vlan.isSlaacEnabled()) summary.append(" SLAAC"); + } + addInfoRow(container, R.drawable.ic_ip_network, + vlan.isUntagged() + ? getString(R.string.network_edit_vlan_untagged) + : fmt("VLAN %d", vlan.getVlanId()), + summary.length() == 0 ? "-" : summary.toString()); + } + var tools = infoData.optJSONArray("tools"); + if (tools != null) + for (int i = 0; i < tools.length(); i++) { + var t = tools.optJSONObject(i); + if (t != null) + addToolRow(container, t.optString("key", ""), + t.optBoolean("running", false)); + } + var pdStatus = infoData.optJSONArray("pd_status"); + if (pdStatus != null) { + for (int i = 0; i < pdStatus.length(); i++) { + var pd = pdStatus.optJSONObject(i); + if (pd == null) continue; + var address = pd.optString("address", ""); addInfoRow(container, R.drawable.ic_ip_network, - getString(R.string.network_info_dhcp_range), range); + fmt("DHCP-PD (%s)", pd.optString("uplink", "")), + address.isEmpty() + ? pd.optString("state", "-") + : fmt("%s (%s)", address, pd.optString("state", ""))); } - } catch (Exception ignored) { } var df = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT); addInfoRow(container, R.drawable.ic_clock, getString(R.string.network_info_created), df.format(new Date(config.item.optLong("created_at", 0)))); } + private boolean hasAnyDhcp() { + if (config == null) return false; + for (var vlan : config.getVlans()) + if (vlan.isDhcp4Enabled() || vlan.isDhcp6Enabled()) return true; + return false; + } + private void bindAddresses(@NonNull LinearLayout container) { + boolean hasAny; + if (hasAddressEntries) { + // Linux bridge: one deduped row per address with VLAN/bound/source chips. + hasAny = addressEntries.length() > 0; + for (int i = 0; i < addressEntries.length(); i++) { + var e = addressEntries.optJSONObject(i); + if (e != null) addAddressEntry(container, e); + } + } else { + // other backends: legacy config + live rendering. + hasAny = bindLegacyAddresses(container); + } + for (int i = 0; i < pdStatus.length(); i++) { + var pd = pdStatus.optJSONObject(i); + if (pd == null) continue; + addPdEntry(container, pd); + hasAny = true; + } + if (!hasAny) addEmptyView(container, R.string.network_info_no_addresses); + } + + private boolean bindLegacyAddresses(@NonNull LinearLayout container) { boolean hasAny = false; if (config != null) { - config.item.get("ipv4_addresses").forEachArray(a -> { - try { - addEntry(container, R.drawable.ic_ip_network, - a.asString(), "IPv4 (configured)"); - } catch (Exception ignored) { + for (var vlan : config.getVlans()) { + var label = vlan.isUntagged() + ? getString(R.string.network_edit_vlan_untagged) + : fmt("VLAN %d", vlan.getVlanId()); + var net4 = vlan.getIpv4Cidr(); + if (net4 != null) { + addEntry(container, R.drawable.ic_ip_network, net4, + fmt("IPv4 (%s)", label)); + hasAny = true; } - }); - config.item.get("ipv6_addresses").forEachArray(a -> { - try { - addEntry(container, R.drawable.ic_ip_network, - a.asString(), "IPv6 (configured)"); - } catch (Exception ignored) { + for (var cidr : vlan.getIpv4Secondary()) { + addEntry(container, R.drawable.ic_ip_network, cidr, + fmt("IPv4 secondary (%s)", label)); + hasAny = true; } - }); - hasAny = !config.item.get("ipv4_addresses").isEmpty() || - !config.item.get("ipv6_addresses").isEmpty(); + var net6 = vlan.getIpv6Cidr(); + if (net6 != null && vlan.getIpv6Source() == Ipv6Source.STATIC) { + addEntry(container, R.drawable.ic_ip_network, net6, + fmt("IPv6 (%s)", label)); + hasAny = true; + } + for (var cidr : vlan.getIpv6Secondary()) { + addEntry(container, R.drawable.ic_ip_network, cidr, + fmt("IPv6 secondary (%s)", label)); + hasAny = true; + } + } } for (int i = 0; i < liveAddresses.length(); i++) { var addr = liveAddresses.optString(i, ""); @@ -483,12 +644,92 @@ private void bindAddresses(@NonNull LinearLayout container) { addr.contains(":") ? "IPv6 (live)" : "IPv4 (live)"); hasAny = true; } - if (!hasAny) addEmptyView(container, R.string.network_info_no_addresses); + return hasAny; + } + + private void addAddressEntry(@NonNull LinearLayout container, @NonNull JSONObject e) { + var vlanLabel = vlanLabel(e.optInt("vlan", 0)); + var bound = getString(e.optBoolean("bound", false) + ? R.string.network_info_addr_bound + : R.string.network_info_addr_unbound); + int sourceRes; + switch (e.optString("source", "")) { + case "configured": sourceRes = R.string.network_info_addr_source_config; break; + case "pbridge_guest": sourceRes = R.string.network_info_addr_source_pbridge; break; + default: sourceRes = R.string.network_info_addr_source_auto; break; + } + var source = getString(sourceRes); + addEntry(container, R.drawable.ic_ip_network, e.optString("addr", ""), + fmt("(%s, %s, %s)", vlanLabel, bound, source)); + } + + private void addPdEntry(@NonNull LinearLayout container, @NonNull JSONObject pd) { + var view = LayoutInflater.from(this) + .inflate(R.layout.item_network_pd, container, false); + var vlan = pd.optInt("vlan", 0); + var state = pd.optString("state", ""); + // The composed host address P::1/L actually bound on the bridge (empty + // while searching) — so this row matches the live nic address and the + // "bound" chip reflects the real binding, not the raw delegation. + var address = pd.optString("address", ""); + var noUplink = "no_uplink".equals(state); + var bound = !address.isEmpty(); + // CIDR area: the bound host address, or the status while there is none + String line1; + if (bound) line1 = address; + else if (noUplink) line1 = getString(R.string.network_info_pd_no_uplink); + else line1 = getString(R.string.network_info_pd_searching); + ((TextView) view.findViewById(R.id.tv_line1)).setText(line1); + // three elements, matching the address rows: (VLAN, bound/unbound, DHCP-PD) + ((TextView) view.findViewById(R.id.tv_line2)).setText(fmt("(%s, %s, %s)", + vlanLabel(vlan), + getString(bound ? R.string.network_info_addr_bound + : R.string.network_info_addr_unbound), + getString(R.string.network_info_pd_label))); + // no PD client exists without an uplink, so release/renew would no-op + MaterialButton release = view.findViewById(R.id.btn_release); + MaterialButton renew = view.findViewById(R.id.btn_renew); + release.setEnabled(!noUplink); + renew.setEnabled(!noUplink); + release.setOnClickListener(v -> sendPdAction("network_pd_release", vlan)); + renew.setOnClickListener(v -> sendPdAction("network_pd_renew", vlan)); + container.addView(view); + } + + private void sendPdAction(@NonNull String command, int vlan) { + DaemonConnection.getInstance().buildRequest(command) + .put("network_id", networkId.toString()) + .put("vlan_id", vlan) + .onResponse(resp -> mainHandler.post(this::syncState)) + .onUnsuccessful(resp -> showError(resp.optString("message", "PD action failed"))) + .onError(e -> showError("PD action failed")) + .invoke(); + } + + @NonNull + private String vlanLabel(int vlan) { + return vlan == 0 + ? getString(R.string.network_edit_vlan_untagged) + : fmt("VLAN %d", vlan); } private void bindPorts(@NonNull LinearLayout container) { + // surface port-forward install failures (e.g. host port already in use) + var failures = infoData.optJSONObject("forward_failures"); + if (failures != null) { + var names = failures.keys(); + while (names.hasNext()) { + var tap = names.next(); + var arr = failures.optJSONArray(tap); + if (arr == null) continue; + for (int i = 0; i < arr.length(); i++) + addEntry(container, R.drawable.ic_close, + getString(R.string.network_info_forward_failed, arr.optString(i, "")), + tap); + } + } if (liveInterfaces.length() == 0) { - addEmptyView(container, R.string.network_info_no_ports); + if (failures == null) addEmptyView(container, R.string.network_info_no_ports); return; } for (int i = 0; i < liveInterfaces.length(); i++) { diff --git a/app/src/main/java/cn/classfun/droidvm/ui/network/info/NetworkToolLogActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/network/info/NetworkToolLogActivity.java new file mode 100644 index 000000000..2659a6a23 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/ui/network/info/NetworkToolLogActivity.java @@ -0,0 +1,94 @@ +package cn.classfun.droidvm.ui.network.info; + +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.view.View; +import android.widget.TextView; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.widget.NestedScrollView; + +import com.google.android.material.appbar.MaterialToolbar; + +import cn.classfun.droidvm.R; +import cn.classfun.droidvm.lib.daemon.DaemonConnection; + +/** + * Read-only viewer for a network helper tool's captured stdout/stderr + * (pbridge / gvswitch / bridgedhcp). Polls the daemon while resumed and + * keeps the view pinned to the bottom unless the user scrolled up. + */ +public final class NetworkToolLogActivity extends AppCompatActivity { + private final Handler handler = new Handler(Looper.getMainLooper()); + private boolean resumed = false; + private String networkId, tool; + private TextView tvLog; + private NestedScrollView scroll; + private final Runnable poll = () -> { + fetch(); + schedule(); + }; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_network_tool_log); + networkId = getIntent().getStringExtra("network_id"); + tool = getIntent().getStringExtra("tool"); + if (networkId == null || tool == null) { + finish(); + return; + } + var title = getIntent().getStringExtra("title"); + MaterialToolbar toolbar = findViewById(R.id.toolbar); + toolbar.setTitle(title != null ? title : tool); + toolbar.setNavigationOnClickListener(v -> finish()); + tvLog = findViewById(R.id.tv_log); + scroll = findViewById(R.id.scroll); + } + + @Override + protected void onResume() { + super.onResume(); + resumed = true; + fetch(); + schedule(); + } + + @Override + protected void onPause() { + super.onPause(); + resumed = false; + handler.removeCallbacks(poll); + } + + private void schedule() { + if (resumed) handler.postDelayed(poll, 2000); + } + + private void fetch() { + DaemonConnection.getInstance().buildRequest("network_tool_log") + .put("network_id", networkId) + .put("tool", tool) + .onResponse(resp -> { + var lines = resp.optJSONArray("lines"); + var sb = new StringBuilder(); + if (lines != null) + for (int i = 0; i < lines.length(); i++) + sb.append(lines.optString(i, "")).append('\n'); + var text = sb.length() == 0 + ? getString(R.string.network_info_tool_log_empty) : sb.toString(); + handler.post(() -> { + if (isFinishing()) return; + boolean atBottom = !scroll.canScrollVertically(1); + tvLog.setText(text); + if (atBottom) scroll.post(() -> scroll.fullScroll(View.FOCUS_DOWN)); + }); + }) + .onUnsuccessful(r -> {}) + .onError(e -> {}) + .invoke(); + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/VMEditActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/VMEditActivity.java index 647e17a80..1f31a22ec 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/VMEditActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/VMEditActivity.java @@ -161,6 +161,11 @@ public List createTabInstances() { } private void doSave() { + // Commit any field still being edited: NIC MAC/offset/forward inputs + // write back to the model on focus loss, so flush the focused view + // before reading the tabs' config. + var focused = getCurrentFocus(); + if (focused != null) focused.clearFocus(); var store = new VMStore(); store.load(this); boolean valid = true; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/base/VMEditTab.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/base/VMEditTab.java index ffe193a83..93a936821 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/base/VMEditTab.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/base/VMEditTab.java @@ -17,7 +17,6 @@ import cn.classfun.droidvm.ui.vm.edit.boot.VMEditBootTab; import cn.classfun.droidvm.ui.vm.edit.graphics.VMEditGraphicsTab; import cn.classfun.droidvm.ui.vm.edit.network.VMEditNetworkTab; -import cn.classfun.droidvm.ui.vm.edit.portforward.VMEditPortForwardTab; import cn.classfun.droidvm.ui.vm.edit.storage.VMEditStorageTab; public enum VMEditTab implements StringEnum { @@ -41,11 +40,6 @@ public enum VMEditTab implements StringEnum { R.id.tab_content_network, VMEditNetworkTab.class ), - TAB_PORT_FORWARD( - R.string.create_vm_tab_port_forward, - R.id.tab_content_port_forward, - VMEditPortForwardTab.class - ), TAB_GRAPHICS( R.string.create_vm_tab_graphics, R.id.tab_content_graphics, diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/network/VMNetEditAdapter.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/network/VMNetEditAdapter.java index 214ca2aa0..7d41a0a0c 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/network/VMNetEditAdapter.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/network/VMNetEditAdapter.java @@ -1,5 +1,7 @@ package cn.classfun.droidvm.ui.vm.edit.network; +import static android.view.View.GONE; +import static android.view.View.VISIBLE; import static cn.classfun.droidvm.lib.utils.NetUtils.generateRandomMac; import static cn.classfun.droidvm.lib.utils.StringUtils.getEditText; @@ -8,6 +10,7 @@ import android.view.View; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.button.MaterialButton; @@ -16,14 +19,31 @@ import java.util.ArrayList; import cn.classfun.droidvm.R; +import cn.classfun.droidvm.lib.store.base.DataItem; +import cn.classfun.droidvm.lib.store.network.NetworkConfig; import cn.classfun.droidvm.lib.store.network.NetworkStore; +import cn.classfun.droidvm.lib.store.network.UplinkMode; +import cn.classfun.droidvm.lib.store.network.VlanConfig; +import cn.classfun.droidvm.lib.store.vm.VMNicConfig; +import cn.classfun.droidvm.lib.store.vm.VMStore; import cn.classfun.droidvm.ui.widgets.container.CardItemAdapter; public final class VMNetEditAdapter extends CardItemAdapter { + private NetworkStore networkStore; + public VMNetEditAdapter(@NonNull Context context) { super(context); } + @NonNull + private NetworkStore networks() { + if (networkStore == null) { + networkStore = new NetworkStore(); + networkStore.load(context); + } + return networkStore; + } + @NonNull @Override protected VMNetEditViewHolder createViewHolderInstance(@NonNull View view) { @@ -38,11 +58,12 @@ protected int getLayoutRes() { @Override public void onBindViewHolder(@NonNull VMNetEditViewHolder holder, int position) { var net = items.get(position); - updateSelectButton(holder.btnSelect, net.optString("network_id", "")); + var nic = new VMNicConfig(net); + updateSelectButton(holder.btnSelect, nic.getNetworkId()); holder.btnSelect.setOnClickListener(v -> { int pos = holder.getBindingAdapterPosition(); if (pos == RecyclerView.NO_POSITION) return; - showNetworkPicker(pos, holder.btnSelect); + showNetworkPicker(pos, holder); }); holder.btnDelete.setOnClickListener(v -> { int pos = holder.getBindingAdapterPosition(); @@ -69,10 +90,325 @@ public void onBindViewHolder(@NonNull VMNetEditViewHolder holder, int position) holder.etMac.setText(randomMac); items.get(pos).set("mac_address", randomMac); }); + bindSwitch(holder, holder.swMacSecurity, "mac_security", + nic.isMacSecurity()); + bindSwitch(holder, holder.swIsolated, "isolated", nic.isIsolated()); + bindVlan(holder, nic); + bindLeases(holder, nic); + } + + private void bindSwitch( + @NonNull VMNetEditViewHolder holder, + @NonNull com.google.android.material.materialswitch.MaterialSwitch sw, + @NonNull String key, boolean value + ) { + sw.setOnCheckedChangeListener(null); + sw.setChecked(value); + sw.setOnCheckedChangeListener((b, checked) -> { + int pos = holder.getBindingAdapterPosition(); + if (pos != RecyclerView.NO_POSITION) + items.get(pos).set(key, checked); + }); + } + + private void bindVlan(@NonNull VMNetEditViewHolder holder, @NonNull VMNicConfig nic) { + var vlanId = nic.getVlanId(); + boolean linux = isLinuxBridge(nic); + holder.swVlan.setOnCheckedChangeListener(null); + holder.swVlan.setChecked(vlanId != null); + applyVlanFieldState(holder, vlanId); + validateVlanField(holder, linux, vlanId == null ? -1 : vlanId); + holder.swVlan.setOnCheckedChangeListener((b, checked) -> { + int pos = holder.getBindingAdapterPosition(); + if (pos == RecyclerView.NO_POSITION) return; + if (checked) { + long id = parseLong(getEditText(holder.etVlan), 0); + if (linux) { + // a Linux bridge has no untagged-only ports: the 4095 + // trunk marker or a leftover 0 become a tagged default + if (id == 4095 || id == 0) id = defaultTaggedVlan(nic); + } else if (id == 4095) { + id = 0; // gvisor: leaving trunk starts at untagged + } + items.get(pos).set("vlan_id", id); + applyVlanFieldState(holder, (int) id); + validateVlanField(holder, linux, id); + } else { + items.get(pos).remove("vlan_id"); + applyVlanFieldState(holder, null); + holder.tilVlan.setError(null); + } + bindLeases(holder, new VMNicConfig(items.get(pos))); + }); + holder.etVlan.setOnFocusChangeListener((v, hasFocus) -> { + if (hasFocus) return; + int pos = holder.getBindingAdapterPosition(); + if (pos == RecyclerView.NO_POSITION) return; + if (holder.swVlan.isChecked()) { + long id = parseLong(getEditText(holder.etVlan), 0); + if (id == 4095) { + // typing the trunk marker means trunk: same as toggling + // off (the listener clears vlan_id and locks the field) + holder.swVlan.setChecked(false); + return; + } + items.get(pos).set("vlan_id", id); + validateVlanField(holder, linux, id); + } + bindLeases(holder, new VMNicConfig(items.get(pos))); + }); + } + + private boolean isLinuxBridge(@NonNull VMNicConfig nic) { + var netId = nic.getNetworkId(); + var network = netId != null ? networks().findById(netId) : null; + return network != null && network.getBridgeType() + == cn.classfun.droidvm.lib.store.network.BridgeType.LINUX; + } + + /** First tagged VLAN configured on the NIC's network, or 1. */ + private int defaultTaggedVlan(@NonNull VMNicConfig nic) { + var netId = nic.getNetworkId(); + var network = netId != null ? networks().findById(netId) : null; + if (network != null) + for (var vlan : network.getVlans()) { + int v = vlan.getVlanId(); + if (v >= 1 && v <= 4094) return v; + } + return 1; + } + + private void validateVlanField( + @NonNull VMNetEditViewHolder holder, boolean linux, long id + ) { + String error = null; + if (linux && id == 0) + error = context.getString(R.string.edit_vm_net_vlan_no_untagged); + else if (id > 4094) + error = context.getString(R.string.edit_vm_net_vlan_invalid); + holder.tilVlan.setError(error); + } + + /** Toggle off = trunk: the ID field locks to the 4095 trunk marker. */ + private static void applyVlanFieldState( + @NonNull VMNetEditViewHolder holder, @Nullable Integer vlanId + ) { + if (vlanId == null) { + holder.etVlan.setText("4095"); + holder.etVlan.setEnabled(false); + holder.tilVlan.setEnabled(false); + } else { + holder.etVlan.setEnabled(true); + holder.tilVlan.setEnabled(true); + holder.etVlan.setText(String.valueOf(vlanId)); + } + } + + /** + * DHCP lease sections are visible for every L3 network; the toggle is + * locked until the effective VLAN actually serves that DHCP family. + */ + private void bindLeases(@NonNull VMNetEditViewHolder holder, @NonNull VMNicConfig nic) { + var netId = nic.getNetworkId(); + var network = netId != null ? networks().findById(netId) : null; + boolean l3 = network != null && network.getUplinkMode() == UplinkMode.L3; + holder.groupDhcp4.setVisibility(l3 ? VISIBLE : GONE); + holder.groupDhcp6.setVisibility(l3 ? VISIBLE : GONE); + if (!l3) return; + var vlan = nic.resolveDhcpVlan(network); + bindLease(holder, nic, vlan, network, false); + bindLease(holder, nic, vlan, network, true); + } + + private void bindLease( + @NonNull VMNetEditViewHolder holder, @NonNull VMNicConfig nic, + @Nullable VlanConfig vlan, @NonNull NetworkConfig network, boolean v6 + ) { + var sw = v6 ? holder.swDhcp6 : holder.swDhcp4; + var detail = v6 ? holder.groupDhcp6Detail : holder.groupDhcp4Detail; + var hint = v6 ? holder.tvDhcp6Hint : holder.tvDhcp4Hint; + var offset = v6 ? holder.etDhcp6Offset : holder.etDhcp4Offset; + var offsetTil = v6 ? holder.tilDhcp6Offset : holder.tilDhcp4Offset; + var fwdTil = v6 ? holder.tilFwd6 : holder.tilFwd4; + var fwd = v6 ? holder.etFwd6 : holder.etFwd4; + var leaseKey = v6 ? "dhcp6_lease" : "dhcp4_lease"; + boolean dhcpServed = vlan != null + && (v6 ? vlan.isDhcp6Enabled() : vlan.isDhcp4Enabled()); + boolean enabled = dhcpServed + && (v6 ? nic.isDhcp6LeaseEnabled() : nic.isDhcp4LeaseEnabled()); + sw.setOnCheckedChangeListener(null); + sw.setChecked(enabled); + sw.setEnabled(dhcpServed); + detail.setVisibility(enabled ? VISIBLE : GONE); + offset.setEnabled(enabled); + offsetTil.setEnabled(enabled); + if (dhcpServed) { + hint.setVisibility(GONE); + } else { + // tell the user which network/VLAN needs DHCP enabled first + int effectiveVlan = nic.getVlanId() != null ? nic.getVlanId() : 0; + var msg = context.getString( + R.string.edit_vm_net_dhcp_need_enable, + v6 ? "6" : "4", network.getName(), effectiveVlan); + if (nic.getVlanId() == null) + msg += context.getString(R.string.edit_vm_net_dhcp_trunk_note); + hint.setText(msg); + hint.setVisibility(VISIBLE); + } + long currentOffset = v6 ? nic.getDhcp6Offset() : nic.getDhcp4Offset(); + offset.setText(String.valueOf(currentOffset)); + fwd.setText(formatForwards( + v6 ? nic.getDhcp6Forwards() : nic.getDhcp4Forwards())); + // IPv6 forwards need IPv6 SNAT, which the Linux bridge can't do + // (Android kernel has no IPv6 NAT) — hide them there entirely + boolean fwdSupported = !v6 + || network.getBridgeType() == cn.classfun.droidvm.lib.store.network.BridgeType.GVISOR; + fwdTil.setVisibility(fwdSupported ? VISIBLE : GONE); + boolean snat = fwdSupported && vlan != null + && (v6 ? vlan.isIpv6Snat() : vlan.isIpv4Snat()); + fwdTil.setEnabled(snat); + fwd.setEnabled(snat); + fwdTil.setHelperText(snat ? context.getString(R.string.edit_vm_net_forwards_hint) + : context.getString(R.string.edit_vm_net_forwards_need_snat)); + var vlanFinal = vlan; + sw.setOnCheckedChangeListener((b, checked) -> { + int pos = holder.getBindingAdapterPosition(); + if (pos == RecyclerView.NO_POSITION) return; + detail.setVisibility(checked ? VISIBLE : GONE); + offset.setEnabled(checked); + offsetTil.setEnabled(checked); + var lease = leaseItem(items.get(pos), leaseKey); + lease.set("enabled", checked); + if (checked && lease.optLong("offset", 0) <= 0 && vlanFinal != null) { + long free = nextFreeOffset(network, vlanFinal, pos, v6); + lease.set("offset", free); + offset.setText(String.valueOf(free)); + } + }); + offset.setOnFocusChangeListener((v, hasFocus) -> { + if (hasFocus) return; + int pos = holder.getBindingAdapterPosition(); + if (pos == RecyclerView.NO_POSITION) return; + leaseItem(items.get(pos), leaseKey) + .set("offset", parseLong(getEditText(offset), 64)); + }); + fwd.setOnFocusChangeListener((v, hasFocus) -> { + if (hasFocus) return; + int pos = holder.getBindingAdapterPosition(); + if (pos == RecyclerView.NO_POSITION) return; + leaseItem(items.get(pos), leaseKey) + .set("forwards", parseForwards(getEditText(fwd))); + }); + // normalize: a lease can't stay enabled once its DHCP is gone + var lease = leaseItem(nic.item, leaseKey); + if (!dhcpServed && lease.optBoolean("enabled", false)) + lease.set("enabled", false); + if (enabled && lease.optLong("offset", 0) <= 0) + lease.set("offset", 64L); + } + + @NonNull + private static DataItem leaseItem(@NonNull DataItem nic, @NonNull String key) { + var lease = nic.opt(key, null); + if (lease == null || !lease.is(DataItem.Type.OBJECT)) { + nic.set(key, DataItem.newObject()); + lease = nic.get(key); + } + return lease; + } + + /** + * The smallest offset >= 64 not used by any other NIC (this VM's other + * NICs or any saved VM) on the same network/VLAN. + */ + private long nextFreeOffset( + @NonNull NetworkConfig network, @NonNull VlanConfig vlan, int selfPos, boolean v6 + ) { + var used = new ArrayList(); + var netId = network.getId().toString(); + int vlanId = vlan.getVlanId(); + for (int i = 0; i < items.size(); i++) { + if (i == selfPos) continue; + collectOffset(used, new VMNicConfig(items.get(i)), netId, vlanId, network, v6); + } + var vmStore = new VMStore(); + vmStore.load(context); + vmStore.forEach((id, vm) -> vm.forEachNic(nic -> + collectOffset(used, nic, netId, vlanId, network, v6))); + long candidate = 64; + while (used.contains(candidate)) candidate++; + return candidate; } - private void showNetworkPicker(int position, MaterialButton btn) { - var netStore = new NetworkStore(); + private static void collectOffset( + @NonNull ArrayList used, @NonNull VMNicConfig nic, + @NonNull String netId, int vlanId, @NonNull NetworkConfig network, boolean v6 + ) { + if (!netId.equals(nic.getNetworkId())) return; + var nicVlan = nic.resolveDhcpVlan(network); + if (nicVlan == null || nicVlan.getVlanId() != vlanId) return; + if (v6) { + if (nic.isDhcp6LeaseEnabled()) used.add(nic.getDhcp6Offset()); + } else { + if (nic.isDhcp4LeaseEnabled()) used.add(nic.getDhcp4Offset()); + } + } + + /** "tcp 80:80" lines <-> forwards array of {proto, host, guest}. */ + @NonNull + static String formatForwards(@NonNull java.util.List forwards) { + var sb = new StringBuilder(); + for (var fwd : forwards) { + if (sb.length() > 0) sb.append('\n'); + if (!fwd.proto.equals("any")) + sb.append(fwd.proto).append(' '); + sb.append(fwd.host).append(':').append(fwd.guest); + } + return sb.toString(); + } + + @NonNull + static DataItem parseForwards(@NonNull String text) { + var arr = DataItem.newArray(); + for (var line : text.split("\n")) { + var trimmed = line.trim(); + if (trimmed.isEmpty()) continue; + // accept "proto host:guest" or just "host:guest" (proto = any = tcp+udp) + String proto = "any"; + String spec = trimmed; + var parts = trimmed.split("\\s+", 2); + if (parts.length == 2) { + var first = parts[0].toLowerCase(); + if (first.equals("tcp") || first.equals("udp")) { + proto = first; + spec = parts[1].trim(); + } + } + var ports = spec.split(":", 2); + if (ports.length != 2) continue; + var host = ports[0].trim(); + var guest = ports[1].trim(); + if (host.isEmpty() || guest.isEmpty()) continue; + var entry = DataItem.newObject(); + entry.set("proto", proto); + entry.set("host", host); + entry.set("guest", guest); + arr.append(entry); + } + return arr; + } + + private static long parseLong(@Nullable String s, long def) { + if (s == null) return def; + try { + return Long.parseLong(s.trim()); + } catch (NumberFormatException e) { + return def; + } + } + + private void showNetworkPicker(int position, @NonNull VMNetEditViewHolder holder) { + var netStore = networks(); netStore.load(context); var ids = new ArrayList(); var names = new ArrayList(); @@ -86,7 +422,8 @@ private void showNetworkPicker(int position, MaterialButton btn) { int checked = Math.max(0, ids.indexOf(currentId)); DialogInterface.OnClickListener onclick = (dialog, which) -> { items.get(position).set("network_id", ids.get(which)); - updateSelectButton(btn, ids.get(which)); + updateSelectButton(holder.btnSelect, ids.get(which)); + bindLeases(holder, new VMNicConfig(items.get(position))); dialog.dismiss(); }; new MaterialAlertDialogBuilder(context) @@ -95,14 +432,12 @@ private void showNetworkPicker(int position, MaterialButton btn) { .show(); } - private void updateSelectButton(MaterialButton btn, String networkId) { + private void updateSelectButton(MaterialButton btn, @Nullable String networkId) { if (networkId == null || networkId.isEmpty()) { btn.setText(R.string.create_vm_network_none); return; } - var netStore = new NetworkStore(); - netStore.load(context); - var net = netStore.findById(networkId); + var net = networks().findById(networkId); btn.setText(net != null ? net.getName() : context.getString(R.string.create_vm_network_none)); } diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/network/VMNetEditViewHolder.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/network/VMNetEditViewHolder.java index 9c477d989..6d2520b03 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/network/VMNetEditViewHolder.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/network/VMNetEditViewHolder.java @@ -2,12 +2,15 @@ import android.view.View; import android.widget.ImageButton; +import android.widget.LinearLayout; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.button.MaterialButton; +import com.google.android.material.materialswitch.MaterialSwitch; import com.google.android.material.textfield.TextInputEditText; +import com.google.android.material.textfield.TextInputLayout; import cn.classfun.droidvm.R; @@ -16,6 +19,27 @@ public final class VMNetEditViewHolder extends RecyclerView.ViewHolder { final ImageButton btnDelete; final TextInputEditText etMac; final ImageButton btnMacRandom; + final MaterialSwitch swMacSecurity; + final MaterialSwitch swIsolated; + final MaterialSwitch swVlan; + final TextInputLayout tilVlan; + final TextInputEditText etVlan; + final LinearLayout groupDhcp4; + final MaterialSwitch swDhcp4; + final View groupDhcp4Detail; + final android.widget.TextView tvDhcp4Hint; + final TextInputEditText etDhcp4Offset; + final TextInputLayout tilDhcp4Offset; + final TextInputLayout tilFwd4; + final TextInputEditText etFwd4; + final LinearLayout groupDhcp6; + final MaterialSwitch swDhcp6; + final View groupDhcp6Detail; + final android.widget.TextView tvDhcp6Hint; + final TextInputEditText etDhcp6Offset; + final TextInputLayout tilDhcp6Offset; + final TextInputLayout tilFwd6; + final TextInputEditText etFwd6; VMNetEditViewHolder(@NonNull View itemView) { super(itemView); @@ -23,5 +47,26 @@ public final class VMNetEditViewHolder extends RecyclerView.ViewHolder { btnDelete = itemView.findViewById(R.id.btn_net_delete); etMac = itemView.findViewById(R.id.et_net_mac); btnMacRandom = itemView.findViewById(R.id.btn_net_mac_random); + swMacSecurity = itemView.findViewById(R.id.sw_nic_mac_security); + swIsolated = itemView.findViewById(R.id.sw_nic_isolated); + swVlan = itemView.findViewById(R.id.sw_nic_vlan); + tilVlan = itemView.findViewById(R.id.til_nic_vlan); + etVlan = itemView.findViewById(R.id.et_nic_vlan); + groupDhcp4 = itemView.findViewById(R.id.group_nic_dhcp4); + swDhcp4 = itemView.findViewById(R.id.sw_nic_dhcp4); + groupDhcp4Detail = itemView.findViewById(R.id.group_nic_dhcp4_detail); + tvDhcp4Hint = itemView.findViewById(R.id.tv_nic_dhcp4_hint); + etDhcp4Offset = itemView.findViewById(R.id.et_nic_dhcp4_offset); + tilDhcp4Offset = itemView.findViewById(R.id.til_nic_dhcp4_offset); + tilFwd4 = itemView.findViewById(R.id.til_nic_fwd4); + etFwd4 = itemView.findViewById(R.id.et_nic_fwd4); + groupDhcp6 = itemView.findViewById(R.id.group_nic_dhcp6); + swDhcp6 = itemView.findViewById(R.id.sw_nic_dhcp6); + groupDhcp6Detail = itemView.findViewById(R.id.group_nic_dhcp6_detail); + tvDhcp6Hint = itemView.findViewById(R.id.tv_nic_dhcp6_hint); + etDhcp6Offset = itemView.findViewById(R.id.et_nic_dhcp6_offset); + tilDhcp6Offset = itemView.findViewById(R.id.til_nic_dhcp6_offset); + tilFwd6 = itemView.findViewById(R.id.til_nic_fwd6); + etFwd6 = itemView.findViewById(R.id.et_nic_fwd6); } } diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/portforward/PortForwardEditAdapter.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/portforward/PortForwardEditAdapter.java deleted file mode 100644 index beae8a645..000000000 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/portforward/PortForwardEditAdapter.java +++ /dev/null @@ -1,174 +0,0 @@ -package cn.classfun.droidvm.ui.vm.edit.portforward; - -import android.content.Context; -import android.content.DialogInterface; -import android.view.View; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; - -import com.google.android.material.button.MaterialButton; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; - -import java.util.ArrayList; - -import cn.classfun.droidvm.R; -import cn.classfun.droidvm.lib.store.base.DataItem; -import cn.classfun.droidvm.lib.store.enums.Enums; -import cn.classfun.droidvm.lib.store.network.NetworkStore; -import cn.classfun.droidvm.lib.store.vm.PortProtocol; -import cn.classfun.droidvm.lib.ui.SimpleTextWatcher; -import cn.classfun.droidvm.ui.widgets.container.CardItemAdapter; - -public final class PortForwardEditAdapter extends CardItemAdapter { - public PortForwardEditAdapter(@NonNull Context context) { - super(context); - } - - @NonNull - @Override - protected PortForwardEditViewHolder createViewHolderInstance(@NonNull View view) { - return new PortForwardEditViewHolder(view); - } - - @Override - protected int getLayoutRes() { - return R.layout.item_port_forward_edit; - } - - @Override - public void onBindViewHolder(@NonNull PortForwardEditViewHolder holder, int position) { - var item = items.get(position); - holder.unbindWatchers(); - - holder.btnProtocol.configure( - PortProtocol.class, Enums.optEnum(item, "protocol", PortProtocol.TCP)); - holder.btnProtocol.setOnValueChangedListener((oldVal, newVal) -> { - int pos = holder.getBindingAdapterPosition(); - if (pos != RecyclerView.NO_POSITION) - items.get(pos).set("protocol", newVal); - }); - - long hostPort = item.optLong("host_port", 0); - holder.etHostPort.setText(hostPort > 0 ? String.valueOf(hostPort) : ""); - holder.hostPortWatcher = SimpleTextWatcher.simpleAfterTextWatcher(s -> { - int pos = holder.getBindingAdapterPosition(); - if (pos != RecyclerView.NO_POSITION) - setPort(items.get(pos), "host_port", s.toString()); - }); - holder.etHostPort.addTextChangedListener(holder.hostPortWatcher); - - // Guest port (empty = same as host port, backend falls back) - long guestPort = item.optLong("guest_port", 0); - holder.etGuestPort.setText(guestPort > 0 ? String.valueOf(guestPort) : ""); - holder.guestPortWatcher = SimpleTextWatcher.simpleAfterTextWatcher(s -> { - int pos = holder.getBindingAdapterPosition(); - if (pos != RecyclerView.NO_POSITION) - setPort(items.get(pos), "guest_port", s.toString()); - }); - holder.etGuestPort.addTextChangedListener(holder.guestPortWatcher); - - holder.switchEnabled.setOnCheckedChangeListener(null); - holder.switchEnabled.setChecked(item.optBoolean("enabled", true)); - holder.switchEnabled.setOnCheckedChangeListener((btn, checked) -> { - int pos = holder.getBindingAdapterPosition(); - if (pos != RecyclerView.NO_POSITION) - items.get(pos).set("enabled", checked); - }); - - holder.etHostIp.setText(item.optString("host_ip", "")); - holder.hostIpWatcher = SimpleTextWatcher.simpleAfterTextWatcher(s -> { - int pos = holder.getBindingAdapterPosition(); - if (pos != RecyclerView.NO_POSITION) - items.get(pos).set("host_ip", s.toString().trim()); - }); - holder.etHostIp.addTextChangedListener(holder.hostIpWatcher); - - holder.etGuestIp.setText(item.optString("guest_ip", "")); - holder.guestIpWatcher = SimpleTextWatcher.simpleAfterTextWatcher(s -> { - int pos = holder.getBindingAdapterPosition(); - if (pos != RecyclerView.NO_POSITION) - items.get(pos).set("guest_ip", s.toString().trim()); - }); - holder.etGuestIp.addTextChangedListener(holder.guestIpWatcher); - - updateNetworkButton(holder.btnNetwork, item.optString("network_id", "")); - holder.btnNetwork.setOnClickListener(v -> { - int pos = holder.getBindingAdapterPosition(); - if (pos != RecyclerView.NO_POSITION) - showNetworkPicker(pos, holder.btnNetwork); - }); - - // Advanced section expand state: expanded by default when advanced values exist (data-driven, safe to reuse) - boolean hasAdvanced = !item.optString("host_ip", "").isEmpty() - || !item.optString("guest_ip", "").isEmpty() - || !item.optString("network_id", "").isEmpty(); - applyAdvancedState(holder, hasAdvanced); - holder.btnAdvanced.setOnClickListener(v -> { - boolean show = holder.layoutAdvanced.getVisibility() != View.VISIBLE; - applyAdvancedState(holder, show); - }); - - holder.btnDelete.setOnClickListener(v -> { - int pos = holder.getBindingAdapterPosition(); - if (pos != RecyclerView.NO_POSITION) - removeItem(pos); - }); - } - - private void applyAdvancedState(@NonNull PortForwardEditViewHolder holder, boolean expanded) { - holder.layoutAdvanced.setVisibility(expanded ? View.VISIBLE : View.GONE); - holder.btnAdvanced.setIconResource( - expanded ? R.drawable.ic_expand_less : R.drawable.ic_expand_more); - } - - /** Writes the port as an integer; empty/invalid becomes 0 (backend: host_port=0 is skipped, guest_port=0 falls back to host_port). */ - private void setPort(@NonNull DataItem item, @NonNull String key, @NonNull String text) { - text = text.trim(); - if (text.isEmpty()) { - item.set(key, 0L); - return; - } - try { - item.set(key, (long) Integer.parseInt(text)); - } catch (NumberFormatException e) { - item.set(key, 0L); - } - } - - private void showNetworkPicker(int position, @NonNull MaterialButton btn) { - var netStore = new NetworkStore(); - netStore.load(context); - var ids = new ArrayList(); - var names = new ArrayList(); - ids.add(""); - names.add(context.getString(R.string.create_vm_network_none)); - netStore.forEach((id, config) -> { - ids.add(id.toString()); - names.add(config.getName()); - }); - var currentId = items.get(position).optString("network_id", ""); - int checked = Math.max(0, ids.indexOf(currentId)); - DialogInterface.OnClickListener onclick = (dialog, which) -> { - items.get(position).set("network_id", ids.get(which)); - updateNetworkButton(btn, ids.get(which)); - dialog.dismiss(); - }; - new MaterialAlertDialogBuilder(context) - .setTitle(R.string.edit_vm_pf_network) - .setSingleChoiceItems(names.toArray(new String[0]), checked, onclick) - .show(); - } - - private void updateNetworkButton(@NonNull MaterialButton btn, String networkId) { - if (networkId == null || networkId.isEmpty()) { - btn.setText(R.string.create_vm_network_none); - return; - } - var netStore = new NetworkStore(); - netStore.load(context); - var net = netStore.findById(networkId); - btn.setText(net != null ? net.getName() : - context.getString(R.string.create_vm_network_none)); - } -} diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/portforward/PortForwardEditViewHolder.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/portforward/PortForwardEditViewHolder.java deleted file mode 100644 index 31e37abab..000000000 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/portforward/PortForwardEditViewHolder.java +++ /dev/null @@ -1,66 +0,0 @@ -package cn.classfun.droidvm.ui.vm.edit.portforward; - -import android.text.TextWatcher; -import android.view.View; -import android.widget.ImageButton; -import android.widget.LinearLayout; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; - -import com.google.android.material.button.MaterialButton; -import com.google.android.material.materialswitch.MaterialSwitch; -import com.google.android.material.textfield.TextInputEditText; - -import cn.classfun.droidvm.R; -import cn.classfun.droidvm.ui.widgets.tools.PickerButtonWidget; - -public final class PortForwardEditViewHolder extends RecyclerView.ViewHolder { - final TextInputEditText etHostPort; - final TextInputEditText etGuestPort; - final TextInputEditText etHostIp; - final TextInputEditText etGuestIp; - final PickerButtonWidget btnProtocol; - final MaterialSwitch switchEnabled; - final MaterialButton btnAdvanced; - final MaterialButton btnNetwork; - final ImageButton btnDelete; - final LinearLayout layoutAdvanced; - TextWatcher hostPortWatcher; - TextWatcher guestPortWatcher; - TextWatcher hostIpWatcher; - TextWatcher guestIpWatcher; - - PortForwardEditViewHolder(@NonNull View itemView) { - super(itemView); - etHostPort = itemView.findViewById(R.id.et_pf_host_port); - etGuestPort = itemView.findViewById(R.id.et_pf_guest_port); - etHostIp = itemView.findViewById(R.id.et_pf_host_ip); - etGuestIp = itemView.findViewById(R.id.et_pf_guest_ip); - btnProtocol = itemView.findViewById(R.id.btn_pf_protocol); - switchEnabled = itemView.findViewById(R.id.switch_pf_enabled); - btnAdvanced = itemView.findViewById(R.id.btn_pf_advanced); - btnNetwork = itemView.findViewById(R.id.btn_pf_network); - btnDelete = itemView.findViewById(R.id.btn_pf_delete); - layoutAdvanced = itemView.findViewById(R.id.layout_pf_advanced); - } - - void unbindWatchers() { - if (hostPortWatcher != null) { - etHostPort.removeTextChangedListener(hostPortWatcher); - hostPortWatcher = null; - } - if (guestPortWatcher != null) { - etGuestPort.removeTextChangedListener(guestPortWatcher); - guestPortWatcher = null; - } - if (hostIpWatcher != null) { - etHostIp.removeTextChangedListener(hostIpWatcher); - hostIpWatcher = null; - } - if (guestIpWatcher != null) { - etGuestIp.removeTextChangedListener(guestIpWatcher); - guestIpWatcher = null; - } - } -} diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/portforward/VMEditPortForwardTab.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/portforward/VMEditPortForwardTab.java deleted file mode 100644 index d0256a9fb..000000000 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/portforward/VMEditPortForwardTab.java +++ /dev/null @@ -1,66 +0,0 @@ -package cn.classfun.droidvm.ui.vm.edit.portforward; - -import static java.util.Objects.requireNonNull; - -import android.view.View; -import android.widget.Toast; - -import androidx.annotation.NonNull; - -import cn.classfun.droidvm.R; -import cn.classfun.droidvm.lib.store.base.DataItem; -import cn.classfun.droidvm.lib.store.vm.VMConfig; -import cn.classfun.droidvm.lib.store.vm.VMStore; -import cn.classfun.droidvm.ui.vm.edit.VMEditActivity; -import cn.classfun.droidvm.ui.vm.edit.base.VMEditBaseTab; -import cn.classfun.droidvm.ui.widgets.container.CardItemListView; - -public final class VMEditPortForwardTab extends VMEditBaseTab { - private CardItemListView listPf; - - public VMEditPortForwardTab(VMEditActivity parent, View view) { - super(parent, view); - } - - @Override - public void initView() { - listPf = view.findViewById(R.id.list_port_forwards); - } - - @Override - public void initValue() { - listPf.setAdapter(PortForwardEditAdapter.class); - } - - @Override - public void loadConfig(@NonNull VMConfig config) { - listPf.setItems(config.item.opt("port_forwards", DataItem.newArray())); - } - - @Override - public boolean validateInput(@NonNull VMStore store) { - if (!rulesValid(requireNonNull(listPf.getItems()))) { - Toast.makeText(parent, R.string.edit_vm_pf_invalid_port, Toast.LENGTH_SHORT).show(); - return false; - } - return true; - } - - /** Mirrors the backend VMPortForwarder.parseRules checks (enabled rules only). */ - public static boolean rulesValid(@NonNull DataItem rules) { - for (var iter : rules) { - var r = iter.getValue(); - if (!r.optBoolean("enabled", true)) continue; - long hostPort = r.optLong("host_port", 0); - if (hostPort <= 0 || hostPort > 65535) return false; - long guestPort = r.optLong("guest_port", 0); - if (guestPort > 65535) return false; // 0 is valid: backend falls back to host_port - } - return true; - } - - @Override - public void saveConfig(@NonNull VMConfig config) { - config.item.set("port_forwards", listPf.getItems()); - } -} diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/info/VMInfoActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/info/VMInfoActivity.java index 57619d103..d3af1bd62 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/info/VMInfoActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/info/VMInfoActivity.java @@ -50,11 +50,9 @@ import cn.classfun.droidvm.lib.store.vm.VMState; import cn.classfun.droidvm.lib.store.vm.VMStore; import cn.classfun.droidvm.lib.ui.UIContext; +import cn.classfun.droidvm.lib.store.network.NetworkStore; import cn.classfun.droidvm.ui.vm.VMActions; import cn.classfun.droidvm.ui.vm.edit.VMEditActivity; -import cn.classfun.droidvm.ui.vm.edit.portforward.PortForwardEditAdapter; -import cn.classfun.droidvm.ui.vm.edit.portforward.VMEditPortForwardTab; -import cn.classfun.droidvm.ui.widgets.container.CardItemListView; import cn.classfun.droidvm.ui.widgets.container.CollapsibleContainer; import cn.classfun.droidvm.ui.widgets.row.TextRowWidget; @@ -87,7 +85,6 @@ public final class VMInfoActivity extends AppCompatActivity implements Foregroun private CollapsibleContainer ccPortForwards; private LinearLayout containerPortForwards; private TextView tvPfEmpty; - private MaterialButton btnPfEdit; public VMState currentState = VMState.STOPPED; public UUID vmId; public VMConfig config; @@ -118,7 +115,6 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { ccPortForwards = findViewById(R.id.cc_port_forwards); containerPortForwards = findViewById(R.id.container_port_forwards); tvPfEmpty = findViewById(R.id.tv_pf_empty); - btnPfEdit = findViewById(R.id.btn_pf_edit); toolbar = findViewById(R.id.toolbar); initialize(); } @@ -143,7 +139,6 @@ private void initialize() { sendControlCommand("powerbtn", vmId, mainHandler, ui)); btnSleepBtn.setOnClickListener(v -> sendControlCommand("sleepbtn", vmId, mainHandler, ui)); - btnPfEdit.setOnClickListener(v -> showPortForwardEditor()); } @Override @@ -317,51 +312,60 @@ private void updateStateUI() { mainHandler.postDelayed(this::refreshPortForwards, 2000); } + /** Renders the per-NIC port forwards configured in the VM's NIC settings. */ private void refreshPortForwards() { - if (currentState != VMState.RUNNING) { + if (config == null) { ccPortForwards.setVisibility(View.GONE); return; } - DaemonConnection.OnResponse res = resp -> { - var data = resp.optJSONObject("data"); - var active = data != null ? data.optJSONArray("active") : null; - mainHandler.post(() -> renderPortForwards(active)); - }; - DaemonConnection.getInstance().buildRequest("vm_port_forward_list") - .put("vm_id", vmId.toString()) - .onResponse(res) - .onUnsuccessful(r -> { - }) - .onError(e -> Log.w(TAG, "Failed to query port forwards", e)) - .invoke(); - } - - private void renderPortForwards(@Nullable JSONArray active) { - if (isFinishing()) return; - if (currentState != VMState.RUNNING) { + var lines = new java.util.ArrayList(); + var networks = new NetworkStore(); + networks.load(this); + config.forEachNic(nic -> { + var netId = nic.getNetworkId(); + var network = netId != null ? networks.findById(netId) : null; + var vlan = network != null ? nic.resolveDhcpVlan(network) : null; + if (vlan == null) return; + if (nic.isDhcp4LeaseEnabled() && vlan.isDhcp4Enabled()) { + var net4 = vlan.getIpv4Network(); + for (var fwd : nic.getDhcp4Forwards()) { + String guest; + try { + guest = net4 != null + ? net4.addressAtOffset(nic.getDhcp4Offset()).toString() + : "?"; + } catch (Exception e) { + guest = "?"; + } + lines.add(fwd.proto.toUpperCase() + " *:" + fwd.host + + " -> " + guest + ":" + fwd.guest); + } + } + if (nic.isDhcp6LeaseEnabled() && vlan.isDhcp6Enabled()) { + var net6 = vlan.getIpv6Network(); + for (var fwd : nic.getDhcp6Forwards()) { + String guest; + try { + guest = net6 != null + ? net6.addressAtOffset(nic.getDhcp6Offset()).toString() + : "?"; + } catch (Exception e) { + guest = "?"; + } + lines.add(fwd.proto.toUpperCase() + " *:" + fwd.host + + " -> [" + guest + "]:" + fwd.guest); + } + } + }); + if (lines.isEmpty()) { ccPortForwards.setVisibility(View.GONE); return; } ccPortForwards.setVisibility(View.VISIBLE); containerPortForwards.removeAllViews(); - int count = active != null ? active.length() : 0; - if (count == 0) { - tvPfEmpty.setVisibility(View.VISIBLE); - return; - } tvPfEmpty.setVisibility(View.GONE); var inflater = LayoutInflater.from(this); - for (int i = 0; i < count; i++) { - var o = active.optJSONObject(i); - if (o == null) continue; - var protocol = o.optString("protocol", "tcp"); - var proto = "udp".equals(protocol) ? "UDP" : "TCP"; - var hostIp = o.optString("host_ip", ""); - var hostPort = o.optInt("host_port", 0); - var guestIp = o.optString("guest_ip", ""); - var guestPort = o.optInt("guest_port", 0); - var hostDisplay = (hostIp.isEmpty() ? "*" : hostIp) + ":" + hostPort; - var line = proto + " " + hostDisplay + " -> " + guestIp + ":" + guestPort; + for (var line : lines) { var row = inflater.inflate( R.layout.item_port_forward_active, containerPortForwards, false); ((TextView) row.findViewById(R.id.tv_pf_line)).setText(line); @@ -369,66 +373,6 @@ private void renderPortForwards(@Nullable JSONArray active) { } } - private void showPortForwardEditor() { - if (config == null) return; - var view = LayoutInflater.from(this).inflate(R.layout.dialog_port_forward_edit, null); - CardItemListView list = view.findViewById(R.id.list_port_forwards); - list.setAdapter(PortForwardEditAdapter.class); - list.setItems(copyArray(config.item.opt("port_forwards", DataItem.newArray()))); - var dialog = new MaterialAlertDialogBuilder(this) - .setTitle(R.string.vm_info_port_forwards) - .setView(view) - .setPositiveButton(R.string.edit_vm_pf_apply, null) - .setNegativeButton(android.R.string.cancel, null) - .create(); - // Override the button so failed validation keeps the dialog open. - dialog.setOnShowListener(d -> dialog.getButton(AlertDialog.BUTTON_POSITIVE) - .setOnClickListener(v -> { - if (applyPortForwardEdits(list)) dialog.dismiss(); - })); - dialog.show(); - } - - private boolean applyPortForwardEdits(CardItemListView list) { - var items = list.getItems(); - if (items == null) items = DataItem.newArray(); - if (!VMEditPortForwardTab.rulesValid(items)) { - Toast.makeText(this, R.string.edit_vm_pf_invalid_port, Toast.LENGTH_SHORT).show(); - return false; - } - config.item.set("port_forwards", items); - runOnPool(() -> store.save(this)); - JSONArray rules; - try { - rules = items.toJsonArray(); - } catch (JSONException e) { - rules = new JSONArray(); - } - DaemonConnection.getInstance().buildRequest("vm_port_forward_set") - .put("vm_id", vmId.toString()) - .put("rules", rules) - .onResponse(r -> { - // Apply may take effect sync (reconcile) or async (loop start); refresh now and after a delay. - mainHandler.post(this::refreshPortForwards); - mainHandler.postDelayed(this::refreshPortForwards, 1500); - }) - .onUnsuccessful(r -> { - }) - .onError(e -> Log.w(TAG, "Failed to set port forwards", e)) - .invoke(); - return true; - } - - /** Deep copy so a cancelled dialog doesn't mutate config. */ - private static DataItem copyArray(DataItem src) { - if (!src.is(DataItem.Type.ARRAY)) return DataItem.newArray(); - try { - return DataItem.fromJson(src.toJsonArray()); - } catch (JSONException e) { - return DataItem.newArray(); - } - } - private void onStartStopClicked() { var isActive = currentState == VMState.RUNNING || currentState == VMState.SUSPENDED; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/widgets/row/SwitchRowWidget.java b/app/src/main/java/cn/classfun/droidvm/ui/widgets/row/SwitchRowWidget.java index 265ff7fd4..5b4146671 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/widgets/row/SwitchRowWidget.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/widgets/row/SwitchRowWidget.java @@ -1,5 +1,6 @@ package cn.classfun.droidvm.ui.widgets.row; +import android.annotation.SuppressLint; import android.content.Context; import android.util.AttributeSet; import android.view.LayoutInflater; @@ -85,6 +86,18 @@ public boolean isChecked() { return switchView.isChecked(); } + /** + * Locks interaction without greying the switch: it keeps its normal + * on/off colour (so a forced-on state reads as on, not disabled) but + * can't be tapped or dragged. Pass true to restore normal interaction. + */ + @SuppressLint("ClickableViewAccessibility") + public void setSwitchEnabled(boolean enabled) { + switchView.setOnTouchListener(enabled ? null : (v, e) -> true); + setOnClickListener(enabled ? v -> switchView.toggle() : null); + setClickable(enabled); + } + public void setChecked(boolean checked) { switchView.setChecked(checked); } diff --git a/app/src/main/res/layout/activity_network_edit.xml b/app/src/main/res/layout/activity_network_edit.xml index eb2ca8e77..999210597 100644 --- a/app/src/main/res/layout/activity_network_edit.xml +++ b/app/src/main/res/layout/activity_network_edit.xml @@ -42,6 +42,16 @@ android:layout_height="wrap_content" android:orientation="vertical"> + + - - - - - - - - - - - + android:hint="@string/network_edit_bridge_type" + android:icon="@drawable/ic_switch" /> - - - + android:hint="@string/network_edit_uplink_mode" + android:icon="@drawable/ic_web_plus" /> - + android:hint="@string/network_edit_l2_uplink" + android:icon="@drawable/ic_ethernet" /> - + android:text="@string/network_edit_pseudo_bridge" + android:icon="@drawable/ic_connection" /> + android:paddingBottom="8dp" + android:textColor="?attr/colorOnSurfaceVariant" + android:textSize="13sp" /> + android:orientation="vertical" /> - - - - - - - - - - - - - - + android:paddingBottom="8dp" + android:text="@string/network_edit_vlan_empty_hint" + android:textColor="?attr/colorOnSurfaceVariant" + android:textSize="13sp" + android:visibility="gone" /> + + @@ -256,10 +194,7 @@ android:layout_height="wrap_content" android:layout_gravity="bottom|end" android:layout_margin="16dp" - android:layout_marginBottom="32dp" - android:contentDescription="@string/network_edit_btn_save" - android:src="@drawable/ic_check" - app:fabSize="normal" - tools:ignore="ClassNameCheck" /> + android:contentDescription="@string/network_edit_save" + android:src="@drawable/ic_check" /> diff --git a/app/src/main/res/layout/activity_network_tool_log.xml b/app/src/main/res/layout/activity_network_tool_log.xml new file mode 100644 index 000000000..cb4b6637e --- /dev/null +++ b/app/src/main/res/layout/activity_network_tool_log.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_vm_edit.xml b/app/src/main/res/layout/activity_vm_edit.xml index 0f503cf47..b51364da8 100644 --- a/app/src/main/res/layout/activity_vm_edit.xml +++ b/app/src/main/res/layout/activity_vm_edit.xml @@ -81,13 +81,6 @@ android:layout_height="wrap_content" android:visibility="gone" /> - - - - - - - - - diff --git a/app/src/main/res/layout/item_network_pd.xml b/app/src/main/res/layout/item_network_pd.xml new file mode 100644 index 000000000..e917f43ef --- /dev/null +++ b/app/src/main/res/layout/item_network_pd.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_network_vlan.xml b/app/src/main/res/layout/item_network_vlan.xml new file mode 100644 index 000000000..2888f98c8 --- /dev/null +++ b/app/src/main/res/layout/item_network_vlan.xml @@ -0,0 +1,367 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_port_forward_edit.xml b/app/src/main/res/layout/item_port_forward_edit.xml deleted file mode 100644 index f5badd167..000000000 --- a/app/src/main/res/layout/item_port_forward_edit.xml +++ /dev/null @@ -1,210 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/item_vm_net_edit.xml b/app/src/main/res/layout/item_vm_net_edit.xml index a0b8c4bae..9113af33c 100644 --- a/app/src/main/res/layout/item_vm_net_edit.xml +++ b/app/src/main/res/layout/item_vm_net_edit.xml @@ -93,6 +93,226 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/partial_vm_edit_port_forward.xml b/app/src/main/res/layout/partial_vm_edit_port_forward.xml deleted file mode 100644 index dd5f659db..000000000 --- a/app/src/main/res/layout/partial_vm_edit_port_forward.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 1ba620f12..87df285b1 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -224,26 +224,22 @@ 移除网络 MAC 地址(可选) 随机 - - 端口 - 端口转发 - 添加规则 - 暂无端口转发规则。 - 删除规则 - 协议 - 主机端口 - Guest 端口 - → - 启用 - 高级 - 主机 IP(可选) - Guest IP(留空自动发现) - 网卡 - 主机端口须为 1-65535 + 源 MAC 過濾 + 端口隔离 + VLAN ID + VLAN + Linux 网桥不支持仅未标记(0)端口,请使用标记 VLAN 或 trunk + VLAN 须为 0–4094,4095 表示 trunk + DHCPv4 静态分配 + DHCPv6 静态分配 + 地址偏移 + 端口转发(例如 tcp 80:80) + 每行一条:[协议] 主机:客户机(不填协议 = tcp+udp),范围如 8000-9000:8000-9000 + 端口转发只在 SNAT 启用时生效 + 请先启用网络「%2$s」VLAN %3$d 的 DHCPv%1$s + (VLAN 4095 匹配网络的 VLAN 0) 端口转发 暂无生效转发 - 编辑转发 - 应用 路径 只读 总线 @@ -570,7 +566,6 @@ 文件图标 正在下载 下载仍在进行中。现在离开将取消下载并删除已下载的部分文件。确定要退出吗? - 已挂起 虚拟机已挂起 @@ -655,6 +650,61 @@ DNS 服务器输入框不为空,请先添加或清空 无效的 DNS 服务器地址 至少需要一个 DNS 服务器 + 保存网络 + 网络正在运行,请先停止后再编辑。 + 网桥类型 + Linux 网桥 + gVisor 网桥 + 外网连接 + 二层桥接 + 三层路由 + 无(虚拟机内网) + 二层桥接 + 三层路由 + 上游接口 + 伪桥接 + Wi-Fi + 以太网 + 网络共享 + 不可用 + %1$s (%2$s) + Wi-Fi 上游无法直接桥接,必须使用伪桥接。 + 此上游可直接桥接,伪桥接为可选项。 + 纯虚拟机内部网络,宿主机不参与。若要参与请添加 VLAN。 + 添加 VLAN + 移除 VLAN + Untagged + VLAN + VLAN ID + 主 IPv4 网络 + 主 IPv6 网络 + 高级 + IPv4 CIDR(例如 10.181.7.1/24) + IPv4 SNAT + DHCPv4 + 地址池偏移起始 + 地址池偏移结束 + IPv6 来源 + 静态 CIDR + DHCP-PD + IPv6 CIDR(例如 fd00:7::1/64) + PD 上游接口 + DUID(留空自动生成) + IPv6 SNAT(仅 gVisor 网桥) + DHCPv6 + SLAAC(::/64) + 次要 IPv4 网络(逗号分隔) + 次要 IPv6 网络(逗号分隔) + DNS 服务器(留空 = 跟随系统 DNS) + 子网 %1$s 与 %2$s 重叠 + %1$s · %2$s + 伪桥接(pbridge) + bridgedhcp + gvswitch + 运行中 + 已停止 + 点按查看日志 + 无日志输出 启动 停止 @@ -679,7 +729,18 @@ 已启用 已禁用 无地址 + 已绑定 + 未绑定 + 配置 + 自动 + 伪桥接VM + DHCP-PD + 搜索中… + 上游接口不可用 + 释放 + 更新 无已连接端口 + 端口转发失败:%1$s 无 DHCP 租约 无邻居 到期: %1$s diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 67e7b2cd5..d973861c7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -226,26 +226,22 @@ Remove network MAC address (optional) Random - - Ports - Port Forwarding - Add Rule - No port forwarding rules. - Remove rule - Protocol - Host Port - Guest Port - → - Enabled - Advanced - Host IP (optional) - Guest IP (auto if empty) - Network - Host port must be 1-65535 + MAC filtering + Isolated + VLAN ID + VLAN + Untagged-only (0) ports are not supported on a Linux bridge; use a tagged VLAN or trunk + VLAN must be 0–4094, or 4095 for trunk + DHCPv4 static lease + DHCPv6 static lease + Address offset + Port forwards (e.g. tcp 80:80) + One per line: [proto] host:guest (no proto = tcp+udp), ranges like 8000-9000:8000-9000 + Port forwards only take effect with SNAT enabled + Enable DHCPv%1$s on network \"%2$s\" VLAN %3$d first + " (VLAN 4095 maps to the network\'s VLAN 0)" Port Forwarding No active forwards - Edit Forwards - Apply Path Read-only Bus @@ -607,7 +603,6 @@ File icon Download in progress A download is still in progress. Leaving now will cancel it and delete the partial file. Are you sure? - Suspended VM is suspended @@ -693,6 +688,61 @@ DNS server input is not empty, please add it first or clear the field Invalid DNS server address At least one DNS server is required + Save network + This network is running. Stop it before editing. + Bridge type + Linux bridge + gVisor bridge + External connectivity + L2 bridging + L3 routing + None (VM-internal) + L2 Bridging + L3 Routing + Upstream interface + Pseudo-bridge + Wi-Fi + Ethernet + Tethering + unavailable + %1$s (%2$s) + Wi-Fi uplinks cannot be bridged directly; the pseudo-bridge is required. + This uplink can be bridged directly; the pseudo-bridge is optional. + VM-internal network: the host does not participate. Add a VLAN for the host to join. + Add VLAN + Remove VLAN + Untagged + VLAN + VLAN ID + Primary IPv4 Network + Primary IPv6 Network + Advanced + IPv4 CIDR (e.g. 10.181.7.1/24) + IPv4 SNAT + DHCPv4 + Pool offset start + Pool offset end + IPv6 source + Static CIDR + DHCP-PD + IPv6 CIDR (e.g. fd00:7::1/64) + PD upstream interface + DUID (auto-generated when empty) + IPv6 SNAT (gVisor bridge only) + DHCPv6 + SLAAC (::/64) + Secondary IPv4 networks (comma separated) + Secondary IPv6 networks (comma separated) + DNS servers (empty = follow system DNS) + Subnets %1$s and %2$s overlap + %1$s · %2$s + Pseudo-bridge (pbridge) + bridgedhcp + gvswitch + Running + Stopped + Tap to view log + No log output Start Stop @@ -718,7 +768,18 @@ Enabled Disabled No addresses + Bound + Unbound + Config + Auto + Pseudo-bridge VM + DHCP-PD + Searching… + Uplink unavailable + Release + Renew No ports connected + Port forward failed: %1$s No DHCP leases No neighbors Expires: %1$s diff --git a/app/src/main/rust/netbox/.gitignore b/app/src/main/rust/netbox/.gitignore new file mode 100644 index 000000000..4f96631da --- /dev/null +++ b/app/src/main/rust/netbox/.gitignore @@ -0,0 +1,2 @@ +/target +/dist diff --git a/app/src/main/rust/netbox/Cargo.lock b/app/src/main/rust/netbox/Cargo.lock new file mode 100644 index 000000000..64a5bf628 --- /dev/null +++ b/app/src/main/rust/netbox/Cargo.lock @@ -0,0 +1,574 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "log" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "netbox" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "futures", + "libc", + "netlink-packet-route", + "netlink-sys", + "rtnetlink", + "serde", + "serde_json", + "tokio", +] + +[[package]] +name = "netlink-packet-core" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3463cbb78394cb0141e2c926b93fc2197e473394b761986eca3b9da2c63ae0f4" +dependencies = [ + "paste", +] + +[[package]] +name = "netlink-packet-route" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be8919612f6028ab4eacbbfe1234a9a43e3722c6e0915e7ff519066991905092" +dependencies = [ + "bitflags", + "libc", + "log", + "netlink-packet-core", +] + +[[package]] +name = "netlink-proto" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b65d130ee111430e47eed7896ea43ca693c387f097dd97376bffafbf25812128" +dependencies = [ + "bytes", + "futures", + "log", + "netlink-packet-core", + "netlink-sys", + "thiserror 2.0.18", +] + +[[package]] +name = "netlink-sys" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6c30ed10fa69cc491d491b85cc971f6bdeb8e7367b7cde2ee6cc878d583fae" +dependencies = [ + "bytes", + "futures-util", + "libc", + "log", + "tokio", +] + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rtnetlink" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc19f84f710fa2f337617f9bc0400260a94224bde7bae28fd8879f3771ca5784" +dependencies = [ + "futures-channel", + "futures-util", + "log", + "netlink-packet-core", + "netlink-packet-route", + "netlink-proto", + "netlink-sys", + "nix", + "thiserror 1.0.69", + "tokio", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/app/src/main/rust/netbox/Cargo.toml b/app/src/main/rust/netbox/Cargo.toml new file mode 100644 index 000000000..e166ce64b --- /dev/null +++ b/app/src/main/rust/netbox/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "netbox" +version = "0.1.0" +edition = "2021" +rust-version = "1.91" +description = "netbox: a minimal netlink CLI toolbox for DroidVM — replaces the iproute2/bridge subset the daemon shells out to, so the APK doesn't depend on each OEM's system ip/bridge (versions vary; some don't even emit IFA_RT_PRIORITY in -j JSON)" + +[[bin]] +name = "netbox" +path = "src/main.rs" + +[dependencies] +clap = { version = "4", features = ["derive"] } +libc = "0.2" +anyhow = "1" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tokio = { version = "1", features = ["rt", "macros", "time"] } +futures = "0.3" +rtnetlink = "0.21" +netlink-packet-route = "0.30" +netlink-sys = "0.8" + +[profile.release] +opt-level = 2 +lto = "thin" +strip = true diff --git a/app/src/main/rust/netbox/build.sh b/app/src/main/rust/netbox/build.sh new file mode 100755 index 000000000..3b3dc9690 --- /dev/null +++ b/app/src/main/rust/netbox/build.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# Build netbox, the daemon's netlink CLI toolbox. +# +# ./build.sh # both targets (default) +# ./build.sh x64 # x86_64-unknown-linux-musl -> dist/netbox-linux-x64 +# ./build.sh arm64 # aarch64-unknown-linux-musl -> dist/netbox-android-arm64 +# ./build.sh host # quick host debug build (./target/debug/netbox) +# +# Both release targets are fully static (musl), so they run under any libc — +# glibc, musl, or Android bionic. The aarch64 target links with the bundled +# rust-lld, so no external cross toolchain is needed. +set -euo pipefail +cd "$(dirname "$0")" + +X64=x86_64-unknown-linux-musl +ARM=aarch64-unknown-linux-musl + +need() { command -v "$1" >/dev/null 2>&1 || { echo "missing: $1" >&2; exit 1; }; } +need cargo +add_target() { command -v rustup >/dev/null 2>&1 && rustup target add "$1" >/dev/null 2>&1 || true; } + +build_x64() { + add_target "$X64" + cargo build --release --target "$X64" + install -Dm755 "target/$X64/release/netbox" dist/netbox-linux-x64 +} + +build_arm() { + add_target "$ARM" + RUSTFLAGS="-C linker=rust-lld" cargo build --release --target "$ARM" + install -Dm755 "target/$ARM/release/netbox" dist/netbox-android-arm64 +} + +case "${1:-all}" in + x64) build_x64 ;; + arm64|aarch64) build_arm ;; + host) cargo build ;; + all) build_x64; build_arm ;; + *) echo "usage: $0 [x64|arm64|host|all]" >&2; exit 1 ;; +esac diff --git a/app/src/main/rust/netbox/src/main.rs b/app/src/main/rust/netbox/src/main.rs new file mode 100644 index 000000000..60f704023 --- /dev/null +++ b/app/src/main/rust/netbox/src/main.rs @@ -0,0 +1,263 @@ +//! netbox — minimal netlink CLI for the DroidVM daemon. +//! +//! The daemon used to shell out to the system `ip`/`bridge` (iproute2). Those +//! vary per OEM and some are too old to emit `IFA_RT_PRIORITY` in `-j` JSON — +//! exactly the field pbridge tags its offload-proxy addresses with. netbox does +//! the same operations straight over rtnetlink, with our own JSON schema, so +//! behaviour is identical on every device. Most invocations do one op and +//! exit (mutations report via exit code, queries print JSON to stdout); the +//! lone exception is `monitor-addr`, which streams a JSON line per change +//! until killed. + +use anyhow::Result; +use clap::{Parser, Subcommand}; +use std::net::IpAddr; + +mod nl; +mod tap; + +#[derive(Parser)] +#[command(name = "netbox", version, about, disable_help_subcommand = true)] +struct Cli { + #[command(subcommand)] + cmd: Cmd, +} + +#[derive(Subcommand)] +enum Cmd { + /// Create a bridge device. + LinkAddBridge { name: String }, + /// Create an 802.1q VLAN subinterface on a parent device. + LinkAddVlan { + #[arg(long)] + link: String, + #[arg(long)] + name: String, + #[arg(long)] + id: u16, + }, + /// Create a persistent TAP (mode tap, vnet_hdr). + LinkAddTap { name: String }, + /// Delete a link by name (idempotent). + LinkDel { name: String }, + /// Bring a link up or down. + LinkSetState { + name: String, + #[arg(value_parser = ["up", "down"])] + state: String, + }, + /// Enslave a link into a bridge. + LinkSetMaster { iface: String, master: String }, + /// Detach a link from its bridge. + LinkSetNomaster { iface: String }, + /// Set a link's MAC address. + LinkSetMac { dev: String, mac: String }, + /// Toggle STP on a bridge. + LinkSetStp { + bridge: String, + #[arg(value_parser = on_off)] + on: bool, + }, + /// Toggle bridge-port isolation. + LinkSetIsolated { + dev: String, + #[arg(value_parser = on_off)] + on: bool, + }, + /// Toggle bridge-port MAC locking. + LinkSetLocked { + dev: String, + #[arg(value_parser = on_off)] + on: bool, + }, + /// Add an address to a device (CIDR). + AddrAdd { dev: String, cidr: String }, + /// Remove an address from a device (CIDR; idempotent). + AddrDel { dev: String, cidr: String }, + /// List addresses as JSON; optional device and IFA_RT_PRIORITY filters. + AddrList { + #[arg(long)] + dev: Option, + #[arg(long)] + metric: Option, + }, + /// Add an onlink subnet route in a table. + RouteAdd { + #[arg(long)] + dev: String, + #[arg(long)] + dst: String, + #[arg(long)] + table: u32, + #[arg(long = "v6")] + v6: bool, + }, + /// Add `from all iif lookup

` (idempotent). + RuleAdd { + #[arg(long)] + iif: String, + #[arg(long)] + table: u32, + #[arg(long = "v6")] + v6: bool, + }, + /// Remove `from all iif lookup
` (idempotent). + RuleDel { + #[arg(long)] + iif: String, + #[arg(long)] + table: u32, + #[arg(long = "v6")] + v6: bool, + }, + /// Add a static fdb entry on a bridge master. + FdbAdd { mac: String, dev: String }, + /// List links as JSON; optional master / bridge-only filters. + LinkList { + #[arg(long)] + master: Option, + #[arg(long = "type-bridge")] + type_bridge: bool, + }, + /// List a device's neighbours as JSON. + NeighList { dev: String }, + /// List policy routing rules as JSON. + RuleList { + #[arg(long = "v6")] + v6: bool, + }, + /// Print the phone's own reachable IPv4 addresses once as `{"v4":[...]}`. + HostIps { + /// Interface-name prefix to include (repeatable): wlan, rmnet_data, … + #[arg(long)] + iface: Vec, + /// Drop addresses tagged with this IFA_RT_PRIORITY (pbridge offload). + #[arg(long)] + exclude_metric: Option, + }, + /// Stream `{"v4":[...]}` whenever the reachable IPv4 set changes. Subscribes + /// to RTNLGRP_IPV4_IFADDR and re-emits only on an actual change; runs until + /// killed. + MonitorAddr { + #[arg(long)] + iface: Vec, + #[arg(long)] + exclude_metric: Option, + }, +} + +fn on_off(s: &str) -> Result { + match s { + "on" => Ok(true), + "off" => Ok(false), + _ => Err(format!("expected on|off, got {s:?}")), + } +} + +/// Split "addr/plen" into its address and prefix length. +fn parse_cidr(cidr: &str) -> Result<(IpAddr, u8)> { + let (a, p) = cidr + .split_once('/') + .ok_or_else(|| anyhow::anyhow!("not a CIDR: {cidr}"))?; + Ok((a.parse()?, p.parse()?)) +} + +fn main() { + std::process::exit(match run() { + Ok(()) => 0, + Err(e) => { + eprintln!("netbox: {e:#}"); + 1 + } + }); +} + +fn run() -> Result<()> { + let cli = Cli::parse(); + // tuntap is a plain ioctl — no netlink runtime needed. + if let Cmd::LinkAddTap { name } = &cli.cmd { + return tap::add_tap(name); + } + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build()?; + rt.block_on(async { + match cli.cmd { + // long-running: its own connection joins a multicast group + Cmd::MonitorAddr { + iface, + exclude_metric, + } => nl::monitor_host_ipv4(&iface, exclude_metric, print_host_ips).await, + // everything else is one op against a fresh connection, then exit + cmd => { + let net = nl::Net::connect()?; + run_oneshot(&net, cmd).await + } + } + }) +} + +async fn run_oneshot(net: &nl::Net, cmd: Cmd) -> Result<()> { + match cmd { + Cmd::LinkAddBridge { name } => net.link_add_bridge(&name).await, + Cmd::LinkAddVlan { link, name, id } => net.link_add_vlan(&link, &name, id).await, + Cmd::LinkAddTap { .. } => unreachable!("handled before runtime"), + Cmd::LinkDel { name } => net.link_del(&name).await, + Cmd::LinkSetState { name, state } => net.link_set_state(&name, state == "up").await, + Cmd::LinkSetMaster { iface, master } => net.link_set_master(&iface, Some(&master)).await, + Cmd::LinkSetNomaster { iface } => net.link_set_master(&iface, None).await, + Cmd::LinkSetMac { dev, mac } => net.link_set_mac(&dev, &mac).await, + Cmd::LinkSetStp { bridge, on } => net.link_set_stp(&bridge, on).await, + Cmd::LinkSetIsolated { dev, on } => net.link_set_isolated(&dev, on).await, + Cmd::LinkSetLocked { dev, on } => net.link_set_locked(&dev, on).await, + Cmd::AddrAdd { dev, cidr } => { + let (ip, plen) = parse_cidr(&cidr)?; + net.addr_add(&dev, ip, plen).await + } + Cmd::AddrDel { dev, cidr } => { + let (ip, plen) = parse_cidr(&cidr)?; + net.addr_del(&dev, ip, plen).await + } + Cmd::AddrList { dev, metric } => print_json(&net.addr_list(dev.as_deref(), metric).await?), + Cmd::RouteAdd { + dev, + dst, + table, + v6: _, + } => { + let (ip, plen) = parse_cidr(&dst)?; + net.route_add(&dev, ip, plen, table).await + } + Cmd::RuleAdd { iif, table, v6 } => net.rule_add(v6, &iif, table).await, + Cmd::RuleDel { iif, table, v6 } => net.rule_del(v6, &iif, table).await, + Cmd::FdbAdd { mac, dev } => net.fdb_add(&mac, &dev).await, + Cmd::LinkList { + master, + type_bridge, + } => print_json(&net.link_list(master.as_deref(), type_bridge).await?), + Cmd::NeighList { dev } => print_json(&net.neigh_list(&dev).await?), + Cmd::RuleList { v6 } => print_json(&net.rule_list(v6).await?), + Cmd::HostIps { + iface, + exclude_metric, + } => print_host_ips(&net.host_ipv4(&iface, exclude_metric).await?), + Cmd::MonitorAddr { .. } => unreachable!("handled in run()"), + } +} + +fn print_json(rows: &T) -> Result<()> { + println!("{}", serde_json::to_string(rows)?); + Ok(()) +} + +/// One JSON line `{"v4":[...]}` flushed immediately — the daemon reads these +/// from the monitor's stdout, so the flush is what makes a change visible +/// without waiting on stdio block-buffering. +fn print_host_ips(v4: &[String]) -> Result<()> { + use std::io::Write; + let line = serde_json::to_string(&serde_json::json!({ "v4": v4 }))?; + let mut out = std::io::stdout().lock(); + writeln!(out, "{line}")?; + out.flush()?; + Ok(()) +} diff --git a/app/src/main/rust/netbox/src/nl.rs b/app/src/main/rust/netbox/src/nl.rs new file mode 100644 index 000000000..5fabf20b7 --- /dev/null +++ b/app/src/main/rust/netbox/src/nl.rs @@ -0,0 +1,762 @@ +//! rtnetlink helpers. Idioms (connection, link/addr parsing, rt_priority, +//! route/rule) are lifted from pbridge's netlink.rs so behaviour matches the +//! proven daemon. Mutations return `Result<()>` (exit code); queries return +//! serializable rows that `main` prints as JSON. + +use anyhow::{bail, Context, Result}; +use futures::stream::TryStreamExt; +use netlink_packet_route::address::{AddressAttribute, AddressScope}; +use netlink_packet_route::link::{ + BridgeStpState, InfoBridge, InfoBridgePort, InfoData, InfoKind, InfoPortData, InfoPortKind, + InfoVlan, LinkAttribute, LinkInfo, LinkMessage, State, +}; +use netlink_packet_route::neighbour::{ + NeighbourAddress, NeighbourAttribute, NeighbourFlags, NeighbourState, +}; +use netlink_packet_route::route::{RouteProtocol, RouteScope}; +use netlink_packet_route::rule::{RuleAction, RuleAttribute, RuleFlags}; +use netlink_packet_route::AddressFamily; +use rtnetlink::{Handle, LinkBridge, LinkUnspec, RouteMessageBuilder}; +use serde::Serialize; +use std::collections::HashMap; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +pub struct Net { + handle: Handle, +} + +/// The only rtnetlink multicast group we ever join: RTM_NEWADDR/RTM_DELADDR +/// for IPv4. The host-IP set is *defined* as v4 addresses, so only address +/// events can change it; staying off the link/route/neigh/v6-addr groups +/// (all noisy and irrelevant) keeps daemon wakeups — and power — to a minimum. +const RTNLGRP_IPV4_IFADDR: u32 = 5; + +// ----------------------------------------------------------------- query rows + +/// `metric` is IFA_RT_PRIORITY — the field the system `ip -j` may not emit but +/// pbridge tags its proxy addresses with; netlink always carries it. +#[derive(Serialize)] +pub struct AddrRow { + pub ifname: String, + pub family: u8, + pub local: String, + pub prefixlen: u8, + pub scope: String, + pub metric: u32, + pub noprefixroute: bool, +} + +#[derive(Serialize)] +pub struct LinkRow { + pub ifname: String, + pub address: String, + pub operstate: String, + pub master: String, + pub mtu: u32, + pub kind: String, +} + +#[derive(Serialize)] +pub struct NeighRow { + pub dst: String, + pub lladdr: String, + pub dev: String, + pub state: Vec, +} + +#[derive(Serialize)] +pub struct RuleRow { + pub priority: u32, + pub iif: Option, + pub table: u32, + pub fwmark: Option, + pub fwmask: Option, + pub detached: bool, + /// action is "lookup
" (vs blackhole/unreachable/...); only these + /// carry a meaningful routing table. + pub lookup: bool, +} + +// ------------------------------------------------------------------- helpers + +fn mac_to_string(b: &[u8]) -> String { + b.iter() + .map(|x| format!("{x:02x}")) + .collect::>() + .join(":") +} + +fn parse_mac(s: &str) -> Result> { + let bytes: Result> = s + .split(':') + .map(|p| u8::from_str_radix(p, 16).with_context(|| format!("bad MAC: {s}"))) + .collect(); + let v = bytes?; + if v.len() != 6 { + bail!("MAC must be 6 octets: {s}"); + } + Ok(v) +} + +fn oper_to_string(s: &State) -> String { + match s { + State::Up => "UP", + State::Down => "DOWN", + State::LowerLayerDown => "LOWERLAYERDOWN", + State::Testing => "TESTING", + State::Dormant => "DORMANT", + State::NotPresent => "NOTPRESENT", + _ => "UNKNOWN", + } + .to_string() +} + +fn neigh_states(s: &NeighbourState) -> Vec { + let name = match s { + NeighbourState::Incomplete => "INCOMPLETE", + NeighbourState::Reachable => "REACHABLE", + NeighbourState::Stale => "STALE", + NeighbourState::Delay => "DELAY", + NeighbourState::Probe => "PROBE", + NeighbourState::Failed => "FAILED", + NeighbourState::Noarp => "NOARP", + NeighbourState::Permanent => "PERMANENT", + _ => "NONE", + }; + vec![name.to_string()] +} + +impl Net { + pub fn connect() -> Result { + let (conn, handle, _m) = rtnetlink::new_connection().context("netlink connect")?; + tokio::spawn(conn); + Ok(Net { handle }) + } + + async fn index_of(&self, name: &str) -> Result { + let mut s = self + .handle + .link() + .get() + .match_name(name.to_string()) + .execute(); + match s.try_next().await { + Ok(Some(m)) => Ok(m.header.index), + Ok(None) => bail!("device not found: {name}"), + Err(e) => Err(e).with_context(|| format!("get_link {name}")), + } + } + + /// index -> ifname for every link (addresses/neighbours carry only ifindex). + async fn link_names(&self) -> Result> { + let mut map = HashMap::new(); + let mut s = self.handle.link().get().execute(); + while let Some(m) = s.try_next().await.context("link dump")? { + map.insert(m.header.index, link_name(&m)); + } + Ok(map) + } + + // ---------------------------------------------------------- link mutations + + pub async fn link_add_bridge(&self, name: &str) -> Result<()> { + self.handle + .link() + .add(LinkBridge::new(name).build()) + .execute() + .await + .context("link_add_bridge") + } + + /// `ip link add link name type vlan id `. + pub async fn link_add_vlan(&self, parent: &str, name: &str, vid: u16) -> Result<()> { + let pidx = self.index_of(parent).await?; + let mut msg = LinkMessage::default(); + msg.attributes.push(LinkAttribute::IfName(name.to_string())); + msg.attributes.push(LinkAttribute::Link(pidx)); + msg.attributes.push(LinkAttribute::LinkInfo(vec![ + LinkInfo::Kind(InfoKind::Vlan), + LinkInfo::Data(InfoData::Vlan(vec![InfoVlan::Id(vid)])), + ])); + self.handle + .link() + .add(msg) + .execute() + .await + .context("link_add_vlan") + } + + pub async fn link_del(&self, name: &str) -> Result<()> { + let idx = match self.index_of(name).await { + Ok(i) => i, + // already gone: idempotent + Err(_) => return Ok(()), + }; + self.handle + .link() + .del(idx) + .execute() + .await + .context("link_del") + } + + pub async fn link_set_state(&self, name: &str, up: bool) -> Result<()> { + let idx = self.index_of(name).await?; + let b = LinkUnspec::new_with_index(idx); + let msg = if up { b.up().build() } else { b.down().build() }; + self.handle + .link() + .set(msg) + .execute() + .await + .context("link_set_state") + } + + pub async fn link_set_master(&self, iface: &str, master: Option<&str>) -> Result<()> { + let idx = self.index_of(iface).await?; + let midx = match master { + Some(m) => self.index_of(m).await?, + None => 0, // nomaster + }; + let msg = LinkUnspec::new_with_index(idx).controller(midx).build(); + self.handle + .link() + .set(msg) + .execute() + .await + .context("link_set_master") + } + + pub async fn link_set_mac(&self, dev: &str, mac: &str) -> Result<()> { + let idx = self.index_of(dev).await?; + let msg = LinkUnspec::new_with_index(idx) + .address(parse_mac(mac)?) + .build(); + self.handle + .link() + .set(msg) + .execute() + .await + .context("link_set_mac") + } + + pub async fn link_set_stp(&self, bridge: &str, on: bool) -> Result<()> { + let idx = self.index_of(bridge).await?; + let mut msg = LinkMessage::default(); + msg.header.index = idx; + let stp = if on { + BridgeStpState::KernelStp + } else { + BridgeStpState::Disabled + }; + msg.attributes.push(LinkAttribute::LinkInfo(vec![ + LinkInfo::Kind(InfoKind::Bridge), + LinkInfo::Data(InfoData::Bridge(vec![InfoBridge::StpState(stp)])), + ])); + self.handle + .link() + .set(msg) + .execute() + .await + .context("link_set_stp") + } + + async fn set_port_flag(&self, dev: &str, port: InfoBridgePort) -> Result<()> { + let idx = self.index_of(dev).await?; + let mut msg = LinkMessage::default(); + msg.header.index = idx; + msg.attributes.push(LinkAttribute::LinkInfo(vec![ + LinkInfo::PortKind(InfoPortKind::Bridge), + LinkInfo::PortData(InfoPortData::BridgePort(vec![port])), + ])); + self.handle + .link() + .set(msg) + .execute() + .await + .context("set_port_flag") + } + + pub async fn link_set_isolated(&self, dev: &str, on: bool) -> Result<()> { + self.set_port_flag(dev, InfoBridgePort::Isolated(on)).await + } + + pub async fn link_set_locked(&self, dev: &str, on: bool) -> Result<()> { + self.set_port_flag(dev, InfoBridgePort::Locked(on)).await + } + + // ---------------------------------------------------------- addr mutations + + pub async fn addr_add(&self, dev: &str, ip: IpAddr, plen: u8) -> Result<()> { + let idx = self.index_of(dev).await?; + let mut req = self.handle.address().add(idx, ip, plen); + // match `ip addr add`: a real IPv4 subnet broadcast (else directed + // broadcast is lost); IPv6 has no broadcast. + if let IpAddr::V4(v4) = ip { + if plen < 31 { + let host = u32::MAX >> plen; + let brd = Ipv4Addr::from(u32::from(v4) | host); + req.message_mut() + .attributes + .push(AddressAttribute::Broadcast(brd)); + } + } + req.execute().await.context("addr_add") + } + + pub async fn addr_del(&self, dev: &str, ip: IpAddr, plen: u8) -> Result<()> { + let idx = self.index_of(dev).await?; + let msg = self + .handle + .address() + .add(idx, ip, plen) + .message_mut() + .clone(); + match self.handle.address().del(msg).execute().await { + Ok(()) => Ok(()), + Err(rtnetlink::Error::NetlinkError(e)) + if e.raw_code() == -libc::EADDRNOTAVAIL || e.raw_code() == -libc::ENOENT => + { + Ok(()) + } + Err(e) => Err(e).context("addr_del"), + } + } + + // --------------------------------------------------------- route mutations + + /// `ip [-6] route add / dev table
` (onlink subnet). + pub async fn route_add(&self, dev: &str, dst: IpAddr, plen: u8, table: u32) -> Result<()> { + let oif = self.index_of(dev).await?; + let msg = match dst { + IpAddr::V4(d) => RouteMessageBuilder::::new() + .destination_prefix(d, plen) + .output_interface(oif) + .table_id(table) + .scope(RouteScope::Link) + .protocol(RouteProtocol::Boot) + .build(), + IpAddr::V6(d) => RouteMessageBuilder::::new() + .destination_prefix(d, plen) + .output_interface(oif) + .table_id(table) + .scope(RouteScope::Link) + .protocol(RouteProtocol::Boot) + .build(), + }; + match self.handle.route().add(msg).execute().await { + Ok(()) => Ok(()), + Err(rtnetlink::Error::NetlinkError(e)) if e.raw_code() == -libc::EEXIST => Ok(()), + Err(e) => Err(e).context("route_add"), + } + } + + // ---------------------------------------------------------- rule mutations + + /// `ip [-4|-6] rule add from all iif lookup
` (idempotent). + /// `.v4()`/`.v6()` yield distinct typed builders, so the family branch + /// can't be factored out (it returns different types) — inline like pbridge. + pub async fn rule_add(&self, v6: bool, iif: &str, table: u32) -> Result<()> { + let req = self + .handle + .rule() + .add() + .input_interface(iif.to_string()) + .table_id(table) + .action(RuleAction::ToTable); + let res = if v6 { + req.v6().execute().await + } else { + req.v4().execute().await + }; + match res { + Ok(()) => Ok(()), + Err(rtnetlink::Error::NetlinkError(e)) if e.raw_code() == -libc::EEXIST => Ok(()), + Err(e) => Err(e).context("rule_add"), + } + } + + /// `ip [-4|-6] rule del from all iif lookup
` (idempotent). + pub async fn rule_del(&self, v6: bool, iif: &str, table: u32) -> Result<()> { + let base = self + .handle + .rule() + .add() + .input_interface(iif.to_string()) + .table_id(table) + .action(RuleAction::ToTable); + let msg = if v6 { + base.v6().message_mut().clone() + } else { + base.v4().message_mut().clone() + }; + match self.handle.rule().del(msg).execute().await { + Ok(()) => Ok(()), + Err(rtnetlink::Error::NetlinkError(e)) + if e.raw_code() == -libc::ENOENT || e.raw_code() == -libc::ESRCH => + { + Ok(()) + } + Err(e) => Err(e).context("rule_del"), + } + } + + // ------------------------------------------------------------ fdb mutation + + /// `bridge fdb add dev master static`: a permanent forwarding + /// entry managed by the bridge master (used for MAC security on locked + /// ports). Idempotent. + pub async fn fdb_add(&self, mac: &str, dev: &str) -> Result<()> { + let idx = self.index_of(dev).await?; + // add_bridge presets family=AF_BRIDGE; "master static" = NTF_MASTER + // (Controller) + NUD_NOARP. + let res = self + .handle + .neighbours() + .add_bridge(idx, &parse_mac(mac)?) + .state(NeighbourState::Noarp) + .flags(NeighbourFlags::Controller) + .execute() + .await; + match res { + Ok(()) => Ok(()), + Err(rtnetlink::Error::NetlinkError(e)) if e.raw_code() == -libc::EEXIST => Ok(()), + Err(e) => Err(e).context("fdb_add"), + } + } + + // -------------------------------------------------------------- queries + + pub async fn addr_list(&self, dev: Option<&str>, metric: Option) -> Result> { + let names = self.link_names().await?; + let mut req = self.handle.address().get(); + if let Some(d) = dev { + req = req.set_link_index_filter(self.index_of(d).await?); + } + let mut s = req.execute(); + let mut out = Vec::new(); + while let Some(m) = s.try_next().await.context("addr dump")? { + let family = match m.header.family { + AddressFamily::Inet => 4u8, + AddressFamily::Inet6 => 6u8, + _ => continue, + }; + let scope = match m.header.scope { + AddressScope::Universe => "global", + AddressScope::Link => "link", + AddressScope::Host => "host", + _ => "other", + } + .to_string(); + let mut local: Option = None; + let mut rt = 0u32; + let mut npr = false; + for a in &m.attributes { + match a { + AddressAttribute::Local(x) => local = Some(*x), + AddressAttribute::Address(x) => { + if local.is_none() { + local = Some(*x); + } + } + AddressAttribute::RoutePriority(p) => rt = *p, + AddressAttribute::Flags(f) => { + use netlink_packet_route::address::AddressFlags; + npr = f.contains(AddressFlags::Noprefixroute); + } + _ => {} + } + } + if let Some(want) = metric { + if rt != want { + continue; + } + } + let local = match local { + Some(x) => x, + None => continue, + }; + out.push(AddrRow { + ifname: names.get(&m.header.index).cloned().unwrap_or_default(), + family, + local: local.to_string(), + prefixlen: m.header.prefix_len, + scope, + metric: rt, + noprefixroute: npr, + }); + } + Ok(out) + } + + /// The phone's own reachable IPv4 addresses, sorted and de-duplicated: + /// scope-global v4 on interfaces whose name starts with one of `prefixes` + /// (the daemon's Wi-Fi/cellular/VPN/ethernet/tethering allowlist), with + /// bridge devices and `exclude_metric`-tagged addresses (pbridge offload) + /// dropped. This is the single definition of the host-IP set, shared by + /// the one-shot `host-ips` and the streaming `monitor-addr`. + pub async fn host_ipv4( + &self, + prefixes: &[String], + exclude_metric: Option, + ) -> Result> { + // one link dump → ifindex's name and whether it's a bridge + let mut names: HashMap = HashMap::new(); + let mut is_bridge: HashMap = HashMap::new(); + let mut ls = self.handle.link().get().execute(); + while let Some(m) = ls.try_next().await.context("link dump")? { + let mut bridge = false; + for a in &m.attributes { + if let LinkAttribute::LinkInfo(infos) = a { + for li in infos { + if let LinkInfo::Kind(InfoKind::Bridge) = li { + bridge = true; + } + } + } + } + names.insert(m.header.index, link_name(&m)); + is_bridge.insert(m.header.index, bridge); + } + let mut out = std::collections::BTreeSet::new(); + let mut s = self.handle.address().get().execute(); + while let Some(m) = s.try_next().await.context("addr dump")? { + if !matches!(m.header.family, AddressFamily::Inet) { + continue; + } + // global scope only: link-local (169.254) / host (127) aren't + // addresses a forward could land on + if !matches!(m.header.scope, AddressScope::Universe) { + continue; + } + let idx = m.header.index; + if *is_bridge.get(&idx).unwrap_or(&false) { + continue; + } + let ifname = names.get(&idx).cloned().unwrap_or_default(); + if !prefixes.iter().any(|p| ifname.starts_with(p.as_str())) { + continue; + } + let mut local: Option = None; + let mut rt = 0u32; + for a in &m.attributes { + match a { + AddressAttribute::Local(x) => local = Some(*x), + AddressAttribute::Address(x) => { + if local.is_none() { + local = Some(*x); + } + } + AddressAttribute::RoutePriority(p) => rt = *p, + _ => {} + } + } + if let Some(want) = exclude_metric { + if rt == want { + continue; + } + } + if let Some(IpAddr::V4(v4)) = local { + out.insert(v4.to_string()); + } + } + Ok(out.into_iter().collect()) + } + + /// `ip -j link show [master
] [type bridge]`. + pub async fn link_list(&self, master: Option<&str>, bridge_only: bool) -> Result> { + let names = self.link_names().await?; + let want_master = match master { + Some(m) => Some(self.index_of(m).await?), + None => None, + }; + let mut s = self.handle.link().get().execute(); + let mut out = Vec::new(); + while let Some(m) = s.try_next().await.context("link dump")? { + let mut address = String::new(); + let mut operstate = "UNKNOWN".to_string(); + let mut master_idx: Option = None; + let mut mtu = 0u32; + let mut kind = String::new(); + for a in &m.attributes { + match a { + LinkAttribute::Address(b) => address = mac_to_string(b), + LinkAttribute::OperState(s) => operstate = oper_to_string(s), + LinkAttribute::Controller(i) => master_idx = Some(*i), + LinkAttribute::Mtu(v) => mtu = *v, + LinkAttribute::LinkInfo(infos) => { + for li in infos { + if let LinkInfo::Kind(k) = li { + kind = format!("{k:?}").to_lowercase(); + } + } + } + _ => {} + } + } + if let Some(wm) = want_master { + if master_idx != Some(wm) { + continue; + } + } + if bridge_only && kind != "bridge" { + continue; + } + out.push(LinkRow { + ifname: link_name(&m), + address, + operstate, + master: master_idx + .and_then(|i| names.get(&i).cloned()) + .unwrap_or_default(), + mtu, + kind, + }); + } + Ok(out) + } + + /// `ip neigh show dev ` (both families). + pub async fn neigh_list(&self, dev: &str) -> Result> { + let idx = self.index_of(dev).await?; + let mut out = Vec::new(); + for v6 in [false, true] { + let mut s = self + .handle + .neighbours() + .get() + .set_family(if v6 { + rtnetlink::IpVersion::V6 + } else { + rtnetlink::IpVersion::V4 + }) + .execute(); + while let Some(m) = s.try_next().await.context("neigh dump")? { + if m.header.ifindex != idx { + continue; + } + let mut dst = String::new(); + let mut lladdr = String::new(); + for a in &m.attributes { + match a { + NeighbourAttribute::Destination(NeighbourAddress::Inet(d)) => { + dst = d.to_string() + } + NeighbourAttribute::Destination(NeighbourAddress::Inet6(d)) => { + dst = d.to_string() + } + NeighbourAttribute::LinkLayerAddress(b) => lladdr = mac_to_string(b), + _ => {} + } + } + if dst.is_empty() { + continue; + } + out.push(NeighRow { + dst, + lladdr, + dev: dev.to_string(), + state: neigh_states(&m.header.state), + }); + } + } + Ok(out) + } + + /// `ip [-6] rule show`. + pub async fn rule_list(&self, v6: bool) -> Result> { + let mut s = self + .handle + .rule() + .get(if v6 { + rtnetlink::IpVersion::V6 + } else { + rtnetlink::IpVersion::V4 + }) + .execute(); + let mut out = Vec::new(); + while let Some(m) = s.try_next().await.context("rule dump")? { + let mut priority = 0u32; + let mut iif = None; + let mut table = m.header.table as u32; + let mut fwmark = None; + let mut fwmask = None; + for a in &m.attributes { + match a { + RuleAttribute::Priority(p) => priority = *p, + RuleAttribute::Iifname(n) => iif = Some(n.clone()), + RuleAttribute::Table(t) => table = *t, + RuleAttribute::FwMark(v) => fwmark = Some(*v), + RuleAttribute::FwMask(v) => fwmask = Some(*v), + _ => {} + } + } + // iproute2 prints [detached] when the iif/oif device is gone. + let detached = m + .header + .flags + .intersects(RuleFlags::IifDetached | RuleFlags::OifDetached); + out.push(RuleRow { + priority, + iif, + table, + fwmark, + fwmask, + detached, + lookup: m.header.action == RuleAction::ToTable, + }); + } + Ok(out) + } +} + +/// Streams the host-IPv4 set: emits the initial set, then re-evaluates and +/// re-emits only when it actually changes, driven by RTNLGRP_IPV4_IFADDR +/// notifications. `emit` is called with each new set (e.g. printing a JSON +/// line); the connection's own diff means the caller is woken only on real +/// changes. Runs until the netlink connection closes. +pub async fn monitor_host_ipv4( + prefixes: &[String], + exclude_metric: Option, + mut emit: impl FnMut(&[String]) -> Result<()>, +) -> Result<()> { + use futures::StreamExt; + use netlink_sys::AsyncSocket; + + let (mut conn, handle, mut messages) = + rtnetlink::new_connection().context("netlink connect")?; + conn.socket_mut() + .socket_mut() + .add_membership(RTNLGRP_IPV4_IFADDR) + .context("join RTNLGRP_IPV4_IFADDR")?; + tokio::spawn(conn); + let net = Net { handle }; + + let mut last = net.host_ipv4(prefixes, exclude_metric).await?; + emit(&last)?; + // every message arrives on RTNLGRP_IPV4_IFADDR, so any of them is a v4 + // address add/del — no need to inspect it, just re-evaluate the set. + while messages.next().await.is_some() { + // coalesce a burst (a DHCP renew is a del+add, etc.) into one recompute + let _ = tokio::time::timeout(std::time::Duration::from_millis(200), async { + while messages.next().await.is_some() {} + }) + .await; + let cur = net.host_ipv4(prefixes, exclude_metric).await?; + if cur != last { + emit(&cur)?; + last = cur; + } + } + Ok(()) +} + +fn link_name(m: &LinkMessage) -> String { + m.attributes + .iter() + .find_map(|a| match a { + LinkAttribute::IfName(n) => Some(n.clone()), + _ => None, + }) + .unwrap_or_default() +} diff --git a/app/src/main/rust/netbox/src/tap.rs b/app/src/main/rust/netbox/src/tap.rs new file mode 100644 index 000000000..c820677ff --- /dev/null +++ b/app/src/main/rust/netbox/src/tap.rs @@ -0,0 +1,48 @@ +//! TAP creation. `ip tuntap add` is not a netlink op — it is a TUNSETIFF ioctl +//! on /dev/net/tun — so it lives apart from the rtnetlink helpers. + +use anyhow::{bail, Context, Result}; +use std::ffi::CString; +use std::os::fd::AsRawFd; + +const IFNAMSIZ: usize = 16; +// — _IOW('T', 202/203, int). The ioctl request arg is +// c_ulong on glibc but c_int on musl/bionic, so cast with `as _` at the call. +const TUNSETIFF: u32 = 0x4004_54ca; +const TUNSETPERSIST: u32 = 0x4004_54cb; +const IFF_TAP: libc::c_short = 0x0002; +const IFF_NO_PI: libc::c_short = 0x1000; +const IFF_VNET_HDR: libc::c_short = 0x4000; + +#[repr(C)] +struct Ifreq { + name: [libc::c_char; IFNAMSIZ], + flags: libc::c_short, + _pad: [u8; 22], +} + +/// `ip tuntap add dev mode tap vnet_hdr`: a persistent TAP with the +/// virtio net header, exactly what the daemon's VM taps require. +pub fn add_tap(name: &str) -> Result<()> { + if name.len() >= IFNAMSIZ { + bail!("tap name too long: {name}"); + } + let f = std::fs::OpenOptions::new() + .read(true) + .write(true) + .open("/dev/net/tun") + .context("open /dev/net/tun")?; + let fd = f.as_raw_fd(); + let mut ifr: Ifreq = unsafe { std::mem::zeroed() }; + for (i, b) in CString::new(name)?.as_bytes().iter().enumerate() { + ifr.name[i] = *b as libc::c_char; + } + ifr.flags = IFF_TAP | IFF_NO_PI | IFF_VNET_HDR; + if unsafe { libc::ioctl(fd, TUNSETIFF as _, &mut ifr as *mut Ifreq) } < 0 { + return Err(std::io::Error::last_os_error()).context("TUNSETIFF"); + } + if unsafe { libc::ioctl(fd, TUNSETPERSIST as _, 1 as libc::c_int) } < 0 { + return Err(std::io::Error::last_os_error()).context("TUNSETPERSIST"); + } + Ok(()) +} diff --git a/app/src/test/java/cn/classfun/droidvm/lib/network/IPNetworkOffsetTest.java b/app/src/test/java/cn/classfun/droidvm/lib/network/IPNetworkOffsetTest.java new file mode 100644 index 000000000..8d6ca22cd --- /dev/null +++ b/app/src/test/java/cn/classfun/droidvm/lib/network/IPNetworkOffsetTest.java @@ -0,0 +1,67 @@ +package cn.classfun.droidvm.lib.network; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import org.junit.Test; + +import java.math.BigInteger; + +public class IPNetworkOffsetTest { + @Test + public void v4OffsetFromBase() { + var net = IPv4Network.parse("10.181.7.1/24"); + assertEquals("10.181.7.64", net.addressAtOffset(64).toString()); + assertEquals("10.181.7.254", net.addressAtOffset(254).toString()); + } + + @Test + public void v4OffsetUsesNetworkBaseNotGateway() { + var net = IPv4Network.parse("10.0.0.5/24"); + assertEquals("10.0.0.64", net.addressAtOffset(64).toString()); + } + + @Test + public void v4OffsetRejectsOutOfRange() { + var net = IPv4Network.parse("10.0.0.1/24"); + assertThrows(IllegalArgumentException.class, () -> net.addressAtOffset(0)); + assertThrows(IllegalArgumentException.class, () -> net.addressAtOffset(255)); + assertThrows(IllegalArgumentException.class, () -> net.addressAtOffset(-1)); + } + + @Test + public void v4OffsetSmallPrefix() { + var net = IPv4Network.parse("192.168.1.1/30"); + assertEquals("192.168.1.2", net.addressAtOffset(2).toString()); + assertThrows(IllegalArgumentException.class, () -> net.addressAtOffset(3)); + } + + @Test + public void v6OffsetFromBase() { + var net = IPv6Network.parse("fd00:7::1/64"); + assertEquals("fd00:7:0:0:0:0:0:40", net.addressAtOffset(64).toString()); + assertEquals( + "fd00:7:0:0:0:0:0:80", + net.addressAtOffset(BigInteger.valueOf(128)).toString() + ); + } + + @Test + public void v6OffsetRejectsOutOfRange() { + var net = IPv6Network.parse("fd00:7::1/64"); + assertThrows(IllegalArgumentException.class, () -> net.addressAtOffset(0)); + assertThrows( + IllegalArgumentException.class, + () -> net.addressAtOffset(BigInteger.ONE.shiftLeft(64)) + ); + } + + @Test + public void v6OffsetLargeValue() { + var net = IPv6Network.parse("fd00::/64"); + assertEquals( + "fd00:0:0:0:ffff:ffff:ffff:fffe", + net.addressAtOffset(BigInteger.ONE.shiftLeft(64).subtract(BigInteger.TWO)).toString() + ); + } +} diff --git a/app/src/test/java/cn/classfun/droidvm/lib/store/network/NetworkConfigValidatorTest.java b/app/src/test/java/cn/classfun/droidvm/lib/store/network/NetworkConfigValidatorTest.java new file mode 100644 index 000000000..45a791bc0 --- /dev/null +++ b/app/src/test/java/cn/classfun/droidvm/lib/store/network/NetworkConfigValidatorTest.java @@ -0,0 +1,162 @@ +package cn.classfun.droidvm.lib.store.network; + +import static org.junit.Assert.assertThrows; + +import androidx.annotation.NonNull; + +import org.junit.Test; + +import cn.classfun.droidvm.lib.store.base.DataItem; + +public class NetworkConfigValidatorTest { + @NonNull + private static NetworkConfig l3Config(@NonNull BridgeType type) { + var config = new NetworkConfig(); + config.setName("test"); + config.setBridgeName("brtest"); + config.setBridgeType(type); + config.setUplinkMode(UplinkMode.L3); + config.l3().set("mac_address", "02:11:22:33:44:55"); + var vlans = DataItem.newArray(); + var vlan = VlanConfig.createDefault(0); + vlan.ipv4().set("cidr", "10.181.7.1/24"); + vlans.append(vlan.item); + config.l3().set("vlans", vlans); + return config; + } + + @Test + public void validL3LinuxPasses() { + NetworkConfigValidator.validate(l3Config(BridgeType.LINUX)); + } + + @Test + public void l3WithZeroVlansPasses() { + // VM-internal network: host doesn't participate (replaces "none") + var config = l3Config(BridgeType.LINUX); + config.l3().set("vlans", DataItem.newArray()); + NetworkConfigValidator.validate(config); + } + + @Test + public void emptyNameRejected() { + var config = l3Config(BridgeType.LINUX); + config.setName(" "); + assertThrows(IllegalArgumentException.class, + () -> NetworkConfigValidator.validate(config)); + } + + @Test + public void badBridgeNameRejected() { + var config = l3Config(BridgeType.LINUX); + config.setBridgeName("0bad name"); + assertThrows(IllegalArgumentException.class, + () -> NetworkConfigValidator.validate(config)); + } + + @Test + public void gatewayAtNetworkAddressRejected() { + var config = l3Config(BridgeType.LINUX); + config.getVlans().get(0).ipv4().set("cidr", "10.181.7.0/24"); + assertThrows(IllegalArgumentException.class, + () -> NetworkConfigValidator.validate(config)); + } + + @Test + public void duplicateVlanIdRejected() { + var config = l3Config(BridgeType.LINUX); + var another = VlanConfig.createDefault(0); + another.ipv4().set("cidr", "10.181.8.1/24"); + config.l3().get("vlans").append(another.item); + assertThrows(IllegalArgumentException.class, + () -> NetworkConfigValidator.validate(config)); + } + + @Test + public void dhcpOffsetsBeyondNetworkRejected() { + var config = l3Config(BridgeType.LINUX); + var dhcp = config.getVlans().get(0).ipv4().get("dhcp"); + dhcp.set("offset_end", 300L); + assertThrows(IllegalArgumentException.class, + () -> NetworkConfigValidator.validate(config)); + } + + @Test + public void ipv6SnatOnLinuxIgnored() { + var config = l3Config(BridgeType.LINUX); + var vlan = config.getVlans().get(0); + vlan.ipv6().set("cidr", "fd00:7::1/64"); + vlan.ipv6().set("snat", true); + NetworkConfigValidator.validate(config); + } + + @Test + public void ipv6SnatOnGvisorPasses() { + var config = l3Config(BridgeType.GVISOR); + var vlan = config.getVlans().get(0); + vlan.ipv6().set("cidr", "fd00:7::1/64"); + vlan.ipv6().set("snat", true); + NetworkConfigValidator.validate(config); + } + + @Test + public void pdOnGvisorRejected() { + var config = l3Config(BridgeType.GVISOR); + var vlan = config.getVlans().get(0); + vlan.ipv6().set("source", "dhcp_pd"); + var pd = DataItem.newObject(); + pd.set("uplink", "WiFi"); + vlan.ipv6().set("pd", pd); + assertThrows(IllegalArgumentException.class, + () -> NetworkConfigValidator.validate(config)); + } + + @Test + public void pdOnLinuxPasses() { + var config = l3Config(BridgeType.LINUX); + var vlan = config.getVlans().get(0); + vlan.ipv6().set("source", "dhcp_pd"); + var pd = DataItem.newObject(); + pd.set("uplink", "WiFi"); + vlan.ipv6().set("pd", pd); + NetworkConfigValidator.validate(config); + } + + @Test + public void secondaryOnGvisorRejected() { + var config = l3Config(BridgeType.GVISOR); + var sec = DataItem.newArray(); + sec.append(new DataItem("192.168.50.1/24")); + config.getVlans().get(0).item.set("ipv4_secondary", sec); + assertThrows(IllegalArgumentException.class, + () -> NetworkConfigValidator.validate(config)); + } + + @Test + public void l2OnGvisorRejected() { + var config = l3Config(BridgeType.GVISOR); + config.setUplinkMode(UplinkMode.L2); + config.l2().set("uplink", "WiFi"); + assertThrows(IllegalArgumentException.class, + () -> NetworkConfigValidator.validate(config)); + } + + @Test + public void l2OnLinuxPasses() { + var config = l3Config(BridgeType.LINUX); + config.setUplinkMode(UplinkMode.L2); + config.l2().set("uplink", "WiFi"); + config.l2().set("pseudo_bridge", "auto"); + NetworkConfigValidator.validate(config); + } + + @Test + public void slaacRequiresSlash64() { + var config = l3Config(BridgeType.LINUX); + var vlan = config.getVlans().get(0); + vlan.ipv6().set("cidr", "fd00:7::1/80"); + vlan.ipv6().get("slaac").set("enabled", true); + assertThrows(IllegalArgumentException.class, + () -> NetworkConfigValidator.validate(config)); + } +} diff --git a/app/src/test/java/cn/classfun/droidvm/lib/store/network/VlanConfigPdTest.java b/app/src/test/java/cn/classfun/droidvm/lib/store/network/VlanConfigPdTest.java new file mode 100644 index 000000000..073acfcce --- /dev/null +++ b/app/src/test/java/cn/classfun/droidvm/lib/store/network/VlanConfigPdTest.java @@ -0,0 +1,57 @@ +package cn.classfun.droidvm.lib.store.network; + +import static org.junit.Assert.assertEquals; + +import androidx.annotation.NonNull; + +import org.junit.Test; + +import cn.classfun.droidvm.lib.store.base.DataItem; + +/** + * PD route-table persistence on the VLAN config: the id is bridgedhcp's + * crash-cleanup key, so a value written through one wrapper must be seen + * by every later read of the same stored item — that is what makes the + * allocation stable across network restarts. + */ +public class VlanConfigPdTest { + @NonNull + private static NetworkConfig configWithVlan() { + var config = new NetworkConfig(); + config.setName("test"); + config.setBridgeName("brtest"); + config.setBridgeType(BridgeType.LINUX); + config.setUplinkMode(UplinkMode.L3); + var vlans = DataItem.newArray(); + var vlan = VlanConfig.createDefault(0); + vlan.ipv4().set("cidr", "10.181.7.1/24"); + vlans.append(vlan.item); + config.l3().set("vlans", vlans); + return config; + } + + @Test + public void routeTableDefaultsToUnallocated() { + var config = configWithVlan(); + assertEquals(0, config.getVlans().get(0).getPdRouteTable()); + } + + @Test + public void routeTablePersistsAcrossWrappers() { + var config = configWithVlan(); + config.getVlans().get(0).setPdRouteTable(9991); + // a fresh wrapper over the same stored item must see the value + assertEquals(9991, config.getVlans().get(0).getPdRouteTable()); + } + + @Test + public void routeTableCoexistsWithDuid() { + var config = configWithVlan(); + var vlan = config.getVlans().get(0); + vlan.setPdDuid("00:03:00:01:aa:bb:cc:dd:ee:ff"); + vlan.setPdRouteTable(9992); + var reread = config.getVlans().get(0); + assertEquals("00:03:00:01:aa:bb:cc:dd:ee:ff", reread.getPdDuid()); + assertEquals(9992, reread.getPdRouteTable()); + } +} diff --git a/app/src/test/java/cn/classfun/droidvm/lib/store/vm/PortForwardTest.java b/app/src/test/java/cn/classfun/droidvm/lib/store/vm/PortForwardTest.java new file mode 100644 index 000000000..7f1f0f1d7 --- /dev/null +++ b/app/src/test/java/cn/classfun/droidvm/lib/store/vm/PortForwardTest.java @@ -0,0 +1,82 @@ +package cn.classfun.droidvm.lib.store.vm; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import org.junit.Test; + +import cn.classfun.droidvm.lib.store.vm.VMNicConfig.PortForward; + +public class PortForwardTest { + @Test + public void singlePort() { + var fwd = new PortForward("tcp", "80", "80"); + fwd.validate(); + assertEquals(80, fwd.hostStart()); + assertEquals(80, fwd.hostEnd()); + } + + @Test + public void portRange() { + var fwd = new PortForward("tcp", "8000-9000", "8000-9000"); + fwd.validate(); + assertEquals(8000, fwd.hostStart()); + assertEquals(9000, fwd.hostEnd()); + assertEquals(8000, fwd.guestStart()); + assertEquals(9000, fwd.guestEnd()); + } + + @Test + public void translatedRange() { + var fwd = new PortForward("udp", "10000-10010", "20000-20010"); + fwd.validate(); + } + + @Test + public void mismatchedRangeSizesRejected() { + var fwd = new PortForward("tcp", "8000-9000", "8000-8500"); + assertThrows(IllegalArgumentException.class, fwd::validate); + } + + @Test + public void reversedRangeRejected() { + var fwd = new PortForward("tcp", "9000-8000", "9000-8000"); + assertThrows(IllegalArgumentException.class, fwd::validate); + } + + @Test + public void anyExpandsToTcpAndUdp() { + var fwd = new PortForward("any", "8080", "80"); + fwd.validate(); + assertEquals(java.util.List.of("tcp", "udp"), fwd.protocols()); + } + + @Test + public void concreteProtoIsSingleton() { + assertEquals(java.util.List.of("udp"), new PortForward("udp", "53", "53").protocols()); + } + + @Test + public void badProtocolRejected() { + var fwd = new PortForward("icmp", "80", "80"); + assertThrows(IllegalArgumentException.class, fwd::validate); + } + + @Test + public void portZeroRejected() { + var fwd = new PortForward("tcp", "0", "0"); + assertThrows(IllegalArgumentException.class, fwd::validate); + } + + @Test + public void portTooLargeRejected() { + var fwd = new PortForward("tcp", "65536", "65536"); + assertThrows(IllegalArgumentException.class, fwd::validate); + } + + @Test + public void garbageRejected() { + var fwd = new PortForward("tcp", "80:80", "80"); + assertThrows(IllegalArgumentException.class, fwd::validate); + } +} From 4a53b87a80870a9e653bae6386167defa24c1b7b Mon Sep 17 00:00:00 2001 From: HuJK Date: Sun, 14 Jun 2026 16:35:09 +0800 Subject: [PATCH 010/182] feat(boot): analyze disk images for direct kernel boot Add lbx-backed boot-entry scanning, BootConfig/BootPlan resolution, a VM boot menu, direct Linux boot UI, and protected-VM DMA warnings for kernels that lack CONFIG_DMA_RESTRICTED_POOL. Expose kernel analysis from URL and LXC image import flows without coupling it to the background download service. Co-authored-by: Claude Opus 4.8 Co-authored-by: Claude Fable 5 --- .../daemon/ipc/vm/BootScanHandler.java | 34 ++ .../droidvm/daemon/ipc/vm/StartHandler.java | 3 + .../classfun/droidvm/daemon/vm/BootPlan.java | 491 +++++++++++++++++ .../droidvm/daemon/vm/VMInstance.java | 35 ++ .../vm/backend/CrosvmBackendInstance.java | 21 +- .../vm/backend/QemuBackendInstance.java | 28 +- .../droidvm/lib/store/vm/BootConfig.java | 276 ++++++++++ .../droidvm/lib/store/vm/VMConfig.java | 29 + .../droidvm/ui/agent/base/AgentVM.java | 11 +- .../ui/disk/download/ImportURLActivity.java | 6 + .../ui/disk/lxc/ImportLxcImagesActivity.java | 9 + .../cn/classfun/droidvm/ui/vm/VMActions.java | 85 +++ .../droidvm/ui/vm/boot/BootEntries.java | 301 ++++++++++ .../droidvm/ui/vm/boot/BootMenuDialog.java | 369 +++++++++++++ .../droidvm/ui/vm/edit/VMEditActivity.java | 8 + .../ui/vm/edit/base/VMEditBaseTab.java | 4 + .../ui/vm/edit/basic/VMEditBasicTab.java | 11 + .../ui/vm/edit/boot/VMEditBootTab.java | 514 ++++++++++++++++-- .../ui/vm/edit/storage/VMEditStorageTab.java | 6 + .../droidvm/ui/vm/info/VMInfoActivity.java | 19 +- .../widgets/tools/KernelAnalysisWidget.java | 192 +++++++ .../res/layout/activity_import_lxc_images.xml | 9 + .../main/res/layout/activity_import_url.xml | 8 + app/src/main/res/layout/dialog_boot_menu.xml | 46 ++ .../main/res/layout/partial_vm_edit_boot.xml | 304 +++++++++-- .../main/res/layout/view_kernel_analysis.xml | 149 +++++ .../main/res/menu/menu_vm_firmware_browse.xml | 14 + app/src/main/res/values-night/colors.xml | 2 + app/src/main/res/values-zh-rCN/strings.xml | 49 ++ app/src/main/res/values/colors.xml | 2 + app/src/main/res/values/strings.xml | 49 ++ 31 files changed, 2984 insertions(+), 100 deletions(-) create mode 100644 app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/BootScanHandler.java create mode 100644 app/src/main/java/cn/classfun/droidvm/daemon/vm/BootPlan.java create mode 100644 app/src/main/java/cn/classfun/droidvm/lib/store/vm/BootConfig.java create mode 100644 app/src/main/java/cn/classfun/droidvm/ui/vm/boot/BootEntries.java create mode 100644 app/src/main/java/cn/classfun/droidvm/ui/vm/boot/BootMenuDialog.java create mode 100644 app/src/main/java/cn/classfun/droidvm/ui/widgets/tools/KernelAnalysisWidget.java create mode 100644 app/src/main/res/layout/dialog_boot_menu.xml create mode 100644 app/src/main/res/layout/view_kernel_analysis.xml create mode 100644 app/src/main/res/menu/menu_vm_firmware_browse.xml diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/BootScanHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/BootScanHandler.java new file mode 100644 index 000000000..1d09eb7dd --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/BootScanHandler.java @@ -0,0 +1,34 @@ +package cn.classfun.droidvm.daemon.ipc.vm; + +import androidx.annotation.NonNull; + +import com.google.auto.service.AutoService; + +import cn.classfun.droidvm.daemon.server.ClientRequest; +import cn.classfun.droidvm.daemon.server.RequestException; +import cn.classfun.droidvm.daemon.server.RequestHandler; +import cn.classfun.droidvm.daemon.vm.BootPlan; + +/** + * Scans a guest disk image for boot entries (lbx) on behalf of the UI: + * the edit tab's detection card and the boot menu both need entry lists, + * and disk images are typically only readable by the daemon. + */ +@AutoService(RequestHandler.class) +public final class BootScanHandler extends RequestHandler { + @NonNull + @Override + public String getName() { + return "vm_bootscan"; + } + + @Override + public void handle(@NonNull ClientRequest request) throws Exception { + var params = request.getParams(); + var image = params.optString("image", ""); + if (image.isEmpty()) + throw new RequestException("missing image"); + var entries = BootPlan.scanEntries(image); + request.res().put("entries", entries); + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/StartHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/StartHandler.java index 97fad35a3..99922c352 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/StartHandler.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/StartHandler.java @@ -32,6 +32,9 @@ public void handle(@NonNull ClientRequest request) throws Exception { if (params.optBoolean("clear_logs_before_start", false) && inst.getState() == VMState.STOPPED) inst.clearLogs(); + // one-shot boot menu selection; empty = follow the stored config + var bootEntry = params.optString("boot_entry", ""); + inst.setBootEntryOverride(bootEntry.isEmpty() ? null : bootEntry); if (!inst.start()) throw new RequestException("failed to start VM"); } diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/BootPlan.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/BootPlan.java new file mode 100644 index 000000000..ffb3fa4f6 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/BootPlan.java @@ -0,0 +1,491 @@ +package cn.classfun.droidvm.daemon.vm; + +import static cn.classfun.droidvm.lib.Constants.DATA_DIR; +import static cn.classfun.droidvm.lib.Constants.PATH_BUILTIN_INITRD; +import static cn.classfun.droidvm.lib.Constants.PATH_BUILTIN_KERNEL; +import static cn.classfun.droidvm.lib.Constants.PATH_EDK2_FIRMWARE; +import static cn.classfun.droidvm.lib.utils.AssetUtils.getAssetBinaryPath; +import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; +import static cn.classfun.droidvm.lib.utils.StringUtils.pathJoin; + +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; + +import cn.classfun.droidvm.lib.natives.NativeProcess; +import cn.classfun.droidvm.lib.store.base.DataItem; +import cn.classfun.droidvm.lib.store.vm.BootConfig; +import cn.classfun.droidvm.lib.store.vm.VMConfig; + +/** + * A VM's boot configuration resolved into concrete backend inputs: + * either "run the UEFI firmware" or "direct-boot this kernel/initrd/ + * cmdline". For {@code linux+image} this is where lbx runs against the + * guest disk: scan entries, match the pinned entry (id, then title, + * then bootloader default), then extract kernel/initrd into a per-VM + * cache. The cache is validated by URI + file size (inode metadata, no + * reads) and then a content MD5 of each source file, so an in-place + * kernel rebuild that kept the same path and size is still caught. The + * MD5 reads the source kernel/initrd out of the image, so a cache hit is + * no longer read-free — but it only happens once the metadata check has + * matched, and a hit still writes nothing. + * + *

A distro {@code vmlinuz} is often a compressed kernel a direct-boot + * VMM can't load as-is (arm64 gzip {@code Image.gz}, or EFI zboot). + * {@code lbx entries} reports this per entry as {@code kernel_compression} + * (null = already a raw {@code Image}/x86 bzImage); when set we extract and + * MD5 the decompressed kernel ({@code lbx cp/md5 --decompress}) so + * the cached file boots directly. lbx stays faithful by default — the flag + * is only passed for a kernel lbx flagged as compressed, never the initrd. + */ +public final class BootPlan { + private static final String TAG = "BootPlan"; + private static final String CACHE_ROOT = pathJoin(DATA_DIR, "cache", "boot"); + private static final long LBX_TIMEOUT_MS = 30_000; + + public final boolean uefi; + /** Custom UEFI firmware path; empty = builtin (QEMU honors, crosvm ignores). */ + @NonNull + public final String firmware; + @NonNull + public final String kernel; + @NonNull + public final String initrd; + @NonNull + public final String cmdline; + /** Image mode: title of the entry actually booted (for events/logs). */ + @Nullable + public final String entryTitle; + /** Image mode: a pinned/override entry was not found, default used. */ + public final boolean entryFallback; + + private BootPlan( + boolean uefi, @NonNull String firmware, @NonNull String kernel, + @NonNull String initrd, @NonNull String cmdline, + @Nullable String entryTitle, boolean entryFallback + ) { + this.uefi = uefi; + this.firmware = firmware; + this.kernel = kernel; + this.initrd = initrd; + this.cmdline = cmdline; + this.entryTitle = entryTitle; + this.entryFallback = entryFallback; + } + + /** + * The plan prepared by {@link VMInstance} before backend start; falls + * back to resolving in place (UEFI/manual never block, and image mode + * resolution throws on scan failure). + */ + @NonNull + public static BootPlan of(@NonNull VMConfig config) { + if (config instanceof VMInstance) { + var plan = ((VMInstance) config).getBootPlan(); + if (plan != null) return plan; + } + try { + return resolve(config, null); + } catch (IOException e) { + throw new RuntimeException(e.getMessage(), e); + } + } + + @NonNull + public static BootPlan resolve( + @NonNull VMConfig config, + @Nullable String entryOverrideId + ) throws IOException { + var boot = BootConfig.of(config); + // one-shot "boot this disk with DroidVM's built-in kernel": a manual + // direct-boot of the bundled kernel/initrd, with root= carried over + // from the image's entry so the same rootfs comes up (see resolveBuiltin) + if (BootConfig.BUILTIN_ENTRY_KEY.equals(entryOverrideId)) + return resolveBuiltin(config, boot); + if (boot.getProtocol() == BootConfig.Protocol.UEFI) + return new BootPlan(true, boot.getUefiFirmware(), "", "", "", null, false); + if (boot.getLinuxSource() == BootConfig.LinuxSource.MANUAL) { + var kernel = boot.getKernel(); + // pre-boot{} configs stored the EDK2 path as the kernel + if (kernel.equals(PATH_EDK2_FIRMWARE)) + return new BootPlan(true, "", "", "", "", null, false); + return new BootPlan( + false, "", kernel, boot.getInitrd(), boot.getCmdline(), null, false + ); + } + return resolveImage(config, boot, entryOverrideId); + } + + /** + * "Boot this disk with DroidVM's built-in kernel": a direct boot of the + * bundled {@code vmlinuz}/{@code initramfs} rather than the guest's own + * kernel. The cmdline is the image cmdline override if set, else the + * resolved entry's effective (vdafix-adjusted) cmdline so the right + * {@code root=} comes up, falling back to {@link BootConfig#DEFAULT_MANUAL_CMDLINE} + * when the image can't be scanned. Persisting this choice instead + * rewrites the config to a plain manual source (see {@code VMActions}). + */ + @NonNull + private static BootPlan resolveBuiltin( + @NonNull VMConfig config, @NonNull BootConfig boot) { + return new BootPlan( + false, "", PATH_BUILTIN_KERNEL, PATH_BUILTIN_INITRD, + builtinCmdline(config, boot), "DroidVM built-in kernel", false + ); + } + + @NonNull + private static String builtinCmdline( + @NonNull VMConfig config, @NonNull BootConfig boot) { + var override = boot.getImageCmdline(); + if (!override.isEmpty()) return override; + try { + var entries = scanEntries(findImagePath(config, boot.getImageDisk())); + if (entries.length() > 0) { + var pinned = boot.getImageEntry(); + var entry = pinned != null + ? matchEntry(entries, pinned.id, pinned.title) : null; + if (entry == null) entry = defaultEntry(entries); + if (entry != null) { + var fixed = boot.isVdafix() ? optStr(entry, "cmdline_fixed") : ""; + var cmdline = !fixed.isEmpty() ? fixed : optStr(entry, "cmdline"); + if (!cmdline.isEmpty()) return cmdline; + } + } + } catch (IOException e) { + Log.w(TAG, fmt( + "builtin-kernel cmdline scan failed, using default: %s", e.getMessage())); + } + return BootConfig.DEFAULT_MANUAL_CMDLINE; + } + + // --- linux+image --- + + @NonNull + private static BootPlan resolveImage( + @NonNull VMConfig config, + @NonNull BootConfig boot, + @Nullable String entryOverrideId + ) throws IOException { + var image = findImagePath(config, boot.getImageDisk()); + var entries = scanEntries(image); + if (entries.length() == 0) + throw new IOException(fmt("no boot entries found in %s", image)); + + var pinned = boot.getImageEntry(); + JSONObject entry = null; + var fallback = false; + if (entryOverrideId != null) + entry = matchEntry(entries, entryOverrideId, entryOverrideId); + else if (pinned != null) + entry = matchEntry(entries, pinned.id, pinned.title); + if (entry == null) { + fallback = entryOverrideId != null || pinned != null; + entry = defaultEntry(entries); + } + if (fallback) + Log.w(TAG, fmt( + "VM %s: pinned boot entry not found in %s, using bootloader default", + config.getName(), image + )); + + var kernelUri = optStr(entry, "kernel"); + if (kernelUri.isEmpty()) + throw new IOException("selected boot entry has no kernel"); + var cacheDir = new File(pathJoin(CACHE_ROOT, config.getId().toString())); + extractCached(image, entry, cacheDir); + + var cmdline = boot.getImageCmdline(); + if (cmdline.isEmpty()) { + var fixed = boot.isVdafix() ? optStr(entry, "cmdline_fixed") : ""; + cmdline = !fixed.isEmpty() ? fixed : optStr(entry, "cmdline"); + } + var initrd = new File(cacheDir, "initrd"); + var title = optStr(entry, "title"); + return new BootPlan( + false, "", + new File(cacheDir, "kernel").getAbsolutePath(), + initrd.exists() ? initrd.getAbsolutePath() : "", + cmdline, + title.isEmpty() ? null : title, + fallback + ); + } + + @NonNull + private static String findImagePath(@NonNull VMConfig config, int diskIndex) throws IOException { + var disks = config.item.opt("disks", null); + if (disks != null && disks.is(DataItem.Type.ARRAY)) { + var arr = disks.asArray(); + // configured index first, then any disk with a path + if (diskIndex >= 0 && diskIndex < arr.size()) { + var path = arr.get(diskIndex).optString("path", ""); + if (!path.isEmpty()) return path; + } + for (var disk : arr) { + var path = disk.optString("path", ""); + if (!path.isEmpty()) return path; + } + } + throw new IOException("boot from disk image: VM has no disk with a path"); + } + + /** All entries of the image, as reported by {@code lbx entries --json}. */ + @NonNull + public static JSONArray scanEntries(@NonNull String image) throws IOException { + var out = runLbx("entries", image, "--json"); + try { + return new JSONArray(out); + } catch (Exception e) { + throw new IOException(fmt("bad lbx output for %s: %s", image, e.getMessage())); + } + } + + @Nullable + private static JSONObject matchEntry( + @NonNull JSONArray entries, + @Nullable String id, + @Nullable String title + ) { + for (int i = 0; i < entries.length(); i++) { + var e = entries.optJSONObject(i); + if (e == null) continue; + if (id != null && !id.isEmpty() && id.equals(optStr(e, "id"))) + return e; + } + for (int i = 0; i < entries.length(); i++) { + var e = entries.optJSONObject(i); + if (e == null) continue; + if (title != null && !title.isEmpty() && title.equals(optStr(e, "title"))) + return e; + } + return null; + } + + /** + * Null-safe string read: org.json's optString turns a JSON null into + * the literal string "null" instead of the fallback. + */ + @NonNull + private static String optStr(@NonNull JSONObject o, @NonNull String key) { + return o.isNull(key) ? "" : o.optString(key, ""); + } + + @NonNull + private static JSONObject defaultEntry(@NonNull JSONArray entries) { + for (int i = 0; i < entries.length(); i++) { + var e = entries.optJSONObject(i); + if (e != null && e.optBoolean("default", false)) return e; + } + return entries.optJSONObject(0); + } + + /** + * Ensures {@code cacheDir} holds this entry's kernel and (concatenated) + * initrd, re-extracting unless {@link #cacheUpToDate} confirms the + * cache still matches the guest (URI+size, then a content MD5). + */ + private static void extractCached( + @NonNull String image, + @NonNull JSONObject entry, + @NonNull File cacheDir + ) throws IOException { + var manifest = new File(cacheDir, "manifest.json"); + var kernelFile = new File(cacheDir, "kernel"); + if (manifest.exists() && kernelFile.exists() + && entry.optLong("kernel_size", -1) > 0 + && cacheUpToDate(image, entry, manifest)) { + Log.i(TAG, fmt("boot cache hit: %s", cacheDir)); + return; + } + + Log.i(TAG, fmt("extracting boot files from %s to %s", image, cacheDir)); + if (!cacheDir.isDirectory() && !cacheDir.mkdirs()) + throw new IOException(fmt("cannot create %s", cacheDir)); + deleteQuiet(manifest); // invalidate while files are in flux + var kernelDest = new File(cacheDir, "kernel").getAbsolutePath(); + if (kernelCompressed(entry)) + runLbx("cp", image, optStr(entry, "kernel"), kernelDest, "--decompress"); + else + runLbx("cp", image, optStr(entry, "kernel"), kernelDest); + var initrds = entry.optJSONArray("initrd"); + var initrdFile = new File(cacheDir, "initrd"); + deleteQuiet(initrdFile); + if (initrds != null && initrds.length() > 0) { + // microcode early-cpio + initramfs: concatenate in order + try (var out = new FileOutputStream(initrdFile)) { + for (int i = 0; i < initrds.length(); i++) { + var part = new File(cacheDir, fmt("initrd.%d", i)); + runLbx("cp", image, initrds.optString(i, ""), part.getAbsolutePath()); + Files.copy(part.toPath(), out); + deleteQuiet(part); + } + } + } + Files.write( + manifest.toPath(), + buildManifest(image, entry).toString().getBytes(StandardCharsets.UTF_8)); + } + + /** + * Whether the cache described by {@code manifest} still reflects the + * guest. Cheap identity first — image, URIs and sizes, all from inode + * metadata with no data reads — then a content MD5 of each source file + * via {@code lbx md5}. The MD5 is what catches an in-place kernel + * rebuild that kept the same path and byte size; it reads the source + * kernel/initrd out of the image, so it only runs once the metadata + * check has already matched (i.e. on an otherwise-unchanged guest). + */ + private static boolean cacheUpToDate( + @NonNull String image, + @NonNull JSONObject entry, + @NonNull File manifestFile + ) { + JSONObject old; + try { + old = new JSONObject(new String( + Files.readAllBytes(manifestFile.toPath()), StandardCharsets.UTF_8)); + } catch (Exception e) { + return false; + } + if (!image.equals(old.optString("image", null))) return false; + var kernelUri = optStr(entry, "kernel"); + if (!kernelUri.equals(old.optString("kernel", null))) return false; + if (entry.optLong("kernel_size", -1) != old.optLong("kernel_size", Long.MIN_VALUE)) + return false; + if (!sameArray(entry.optJSONArray("initrd"), old.optJSONArray("initrd"))) + return false; + if (!sameArray(entry.optJSONArray("initrd_size"), old.optJSONArray("initrd_size"))) + return false; + try { + if (!lbxMd5(image, kernelUri, kernelCompressed(entry)) + .equals(old.optString("kernel_md5", ""))) + return false; + var initrds = entry.optJSONArray("initrd"); + var oldMd5 = old.optJSONArray("initrd_md5"); + int n = initrds == null ? 0 : initrds.length(); + if (oldMd5 == null || oldMd5.length() != n) return false; + for (int i = 0; i < n; i++) + if (!lbxMd5(image, initrds.optString(i, "")).equals(oldMd5.optString(i, ""))) + return false; + } catch (IOException e) { + // lbx md5 unavailable or failed: don't trust the cache + Log.w(TAG, fmt("cache md5 check failed, re-extracting: %s", e.getMessage())); + return false; + } + return true; + } + + /** Metadata key plus a content MD5 of each source file. */ + @NonNull + private static JSONObject buildManifest( + @NonNull String image, @NonNull JSONObject entry) throws IOException { + var key = new JSONObject(); + try { + var kernelUri = optStr(entry, "kernel"); + key.put("image", image); + key.put("kernel", kernelUri); + key.put("kernel_size", entry.optLong("kernel_size", -1)); + key.put("kernel_md5", lbxMd5(image, kernelUri, kernelCompressed(entry))); + var initrds = entry.optJSONArray("initrd"); + key.put("initrd", initrds); + key.put("initrd_size", entry.optJSONArray("initrd_size")); + var md5s = new JSONArray(); + if (initrds != null) + for (int i = 0; i < initrds.length(); i++) + md5s.put(lbxMd5(image, initrds.optString(i, ""))); + key.put("initrd_md5", md5s); + } catch (Exception e) { + throw new IOException(e.getMessage()); + } + return key; + } + + /** + * Whether this entry's kernel is a compressed {@code vmlinuz} (gzip + * {@code Image.gz} or EFI zboot) that a direct-kernel-boot VMM can't + * load as-is, per lbx's {@code kernel_compression} field (empty/null + * when the kernel is already a raw {@code Image} or x86 bzImage). When + * set, the kernel is extracted and MD5'd in its decompressed form. + */ + private static boolean kernelCompressed(@NonNull JSONObject entry) { + return !optStr(entry, "kernel_compression").isEmpty(); + } + + /** The hex digest from {@code lbx md5 } (" "). */ + @NonNull + private static String lbxMd5(@NonNull String image, @NonNull String uri) + throws IOException { + return lbxMd5(image, uri, false); + } + + /** + * As {@link #lbxMd5(String, String)}, but with {@code decompress} the + * digest is of the kernel as {@code cp --decompress} would write it, so + * it matches the extracted cache file rather than the raw stored bytes. + */ + @NonNull + private static String lbxMd5(@NonNull String image, @NonNull String uri, boolean decompress) + throws IOException { + if (uri.isEmpty()) return ""; + var out = (decompress + ? runLbx("md5", image, uri, "--decompress") + : runLbx("md5", image, uri)).trim(); + int sp = out.indexOf(' '); + return sp > 0 ? out.substring(0, sp) : out; + } + + private static boolean sameArray( + @Nullable JSONArray a, @Nullable JSONArray b) { + return String.valueOf(a).equals(String.valueOf(b)); + } + + private static void deleteQuiet(@NonNull File f) { + if (f.exists() && !f.delete()) + Log.w(TAG, fmt("failed to delete %s", f)); + } + + /** Runs lbx, returning stdout; non-zero exit becomes an IOException. */ + @NonNull + private static String runLbx(@NonNull String... args) throws IOException { + var argv = new String[args.length + 1]; + argv[0] = getAssetBinaryPath("lbx"); + System.arraycopy(args, 0, argv, 1, args.length); + var process = new NativeProcess.Builder(argv).start(); + try { + var stdout = new String( + process.getInputStream().readAllBytes(), StandardCharsets.UTF_8); + var stderr = new String( + process.getErrorStream().readAllBytes(), StandardCharsets.UTF_8); + int code; + try { + if (!process.waitFor(LBX_TIMEOUT_MS, java.util.concurrent.TimeUnit.MILLISECONDS)) { + process.destroy(); + throw new IOException(fmt("lbx timed out: %s", String.join(" ", argv))); + } + code = process.exitValue(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException("interrupted while running lbx"); + } + if (code != 0) + throw new IOException(fmt( + "lbx failed (%d): %s", code, stderr.trim().isEmpty() + ? String.join(" ", argv) : stderr.trim() + )); + return stdout; + } finally { + process.close(); + } + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMInstance.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMInstance.java index 8bb270bec..8ba497b73 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMInstance.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMInstance.java @@ -43,6 +43,8 @@ public final class VMInstance extends VMConfig { private VMBackendInstance backendInstance; private Thread workerThread; private final VMInstanceStore store; + private BootPlan bootPlan; + private String bootEntryOverride; public interface VMEventCallback { @SuppressWarnings("unused") @@ -121,6 +123,21 @@ public boolean runControlCommand(@NonNull String command) { return getBackendInstance().runControlCommand(command) == 0; } + /** Boot plan prepared for the current start; null when not started. */ + @Nullable + public BootPlan getBootPlan() { + return bootPlan; + } + + /** + * One-shot boot entry selection (id or title) from the GUI boot menu; + * applies to the next {@link #start()} only and does not touch the + * pinned entry in the config. + */ + public void setBootEntryOverride(@Nullable String entryId) { + bootEntryOverride = entryId; + } + @SuppressWarnings("BooleanMethodIsAlwaysInverted") public boolean start() { if (state != VMState.STOPPED) { @@ -317,6 +334,24 @@ public void clearLogs() { private void runVM() { var inst = getBackendInstance(); var vmId = getId().toString(); + try { + // image-source boot runs lbx here (scan + cached extract), so + // backends only ever see resolved kernel/initrd/cmdline paths + bootPlan = BootPlan.resolve(this, bootEntryOverride); + } catch (Exception e) { + Log.e(TAG, fmt("VM %s: boot resolution failed", getName()), e); + var sio = getStream("stdio"); + if (sio != null) + sio.appendBuffer(fmt("boot resolution failed: %s\n", e.getMessage())); + exitCode = -1; + setState(VMState.STOPPED); + fireEvent("exited", null); + return; + } finally { + bootEntryOverride = null; + } + if (bootPlan.entryFallback) + fireEvent("boot_entry_fallback", null); var result = inst.start(); if (!result.isSuccess()) { exitCode = -1; diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackendInstance.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackendInstance.java index 83f695194..10f13cac1 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackendInstance.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackendInstance.java @@ -28,6 +28,7 @@ import cn.classfun.droidvm.daemon.console.FDPipeConsoleStream; import cn.classfun.droidvm.daemon.console.InputConsoleStream; import cn.classfun.droidvm.daemon.console.SimpleConsoleStream; +import cn.classfun.droidvm.daemon.vm.BootPlan; import cn.classfun.droidvm.daemon.vm.SerialPipe; import cn.classfun.droidvm.daemon.vm.VMBackendInstance; import cn.classfun.droidvm.daemon.vm.VMStartResult; @@ -150,15 +151,14 @@ private List buildCommand() { args.add("--hugepages"); if (item.optBoolean("prepare_lend_mthp", true)) args.add("--prepare-lend-mthp"); - var initrd = item.optString("initrd", ""); - if (!initrd.isEmpty()) { + var boot = BootPlan.of(config); + if (!boot.initrd.isEmpty()) { args.add("--initrd"); - args.add(initrd); + args.add(boot.initrd); } - var cmdline = item.optString("cmdline", ""); - if (!cmdline.isEmpty()) { + if (!boot.cmdline.isEmpty()) { args.add("--params"); - args.add(cmdline); + args.add(boot.cmdline); } if (controlSocketPath != null) { args.add("--socket"); @@ -170,12 +170,11 @@ private List buildCommand() { buildGpuCommand(args); buildVncCommand(args); buildSerialCommand(args); - if (item.optBoolean("use_uefi", true)) { + if (boot.uefi) { + // crosvm has no custom-firmware support; always builtin EDK2 args.add(PATH_EDK2_FIRMWARE); - } else { - var kernel = item.optString("kernel", ""); - if (!kernel.isEmpty()) - args.add(kernel); + } else if (!boot.kernel.isEmpty()) { + args.add(boot.kernel); } return args; } diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/QemuBackendInstance.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/QemuBackendInstance.java index d4619efed..109d372f2 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/QemuBackendInstance.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/QemuBackendInstance.java @@ -3,7 +3,6 @@ import static android.net.LocalSocketAddress.Namespace.FILESYSTEM; import static java.nio.charset.StandardCharsets.UTF_8; import static cn.classfun.droidvm.lib.Constants.DATA_DIR; -import static cn.classfun.droidvm.lib.Constants.PATH_EDK2_FIRMWARE; import static cn.classfun.droidvm.lib.Constants.PATH_EDK2_QEMU_FIRMWARE; import static cn.classfun.droidvm.lib.store.enums.Enums.optEnum; import static cn.classfun.droidvm.lib.utils.AssetUtils.getPrebuiltBinaryPath; @@ -30,6 +29,7 @@ import cn.classfun.droidvm.daemon.console.InputConsoleStream; import cn.classfun.droidvm.daemon.console.LocalSocketConsoleStream; import cn.classfun.droidvm.daemon.console.SimpleConsoleStream; +import cn.classfun.droidvm.daemon.vm.BootPlan; import cn.classfun.droidvm.daemon.vm.VMBackendInstance; import cn.classfun.droidvm.daemon.vm.VMStartResult; import cn.classfun.droidvm.lib.natives.NativeProcess; @@ -158,30 +158,22 @@ private List buildCommand() { args.add("arm-confidential-guest,id=prot0,swiotlb-size=64M"); break; } - if (item.optBoolean("use_uefi", true)) { + var boot = BootPlan.of(config); + if (boot.uefi) { args.add("-bios"); - args.add(PATH_EDK2_QEMU_FIRMWARE); - } else { - var bios = item.optString("bios", ""); - if (!bios.isEmpty()) { - args.add("-bios"); - args.add(bios); - } + args.add(boot.firmware.isEmpty() ? PATH_EDK2_QEMU_FIRMWARE : boot.firmware); } - var kernel = item.optString("kernel", ""); - if (!kernel.isEmpty() && !kernel.equals(PATH_EDK2_FIRMWARE)) { + if (!boot.kernel.isEmpty()) { args.add("-kernel"); - args.add(kernel); + args.add(boot.kernel); } - var initrd = item.optString("initrd", ""); - if (!initrd.isEmpty()) { + if (!boot.initrd.isEmpty()) { args.add("-initrd"); - args.add(initrd); + args.add(boot.initrd); } - var cmdline = item.optString("cmdline", ""); - if (!cmdline.isEmpty()) { + if (!boot.cmdline.isEmpty()) { args.add("-append"); - args.add(cmdline); + args.add(boot.cmdline); } if (item.optBoolean("hugepages", true)) args.add("-mem-prealloc"); diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/BootConfig.java b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/BootConfig.java new file mode 100644 index 000000000..4d86dab70 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/BootConfig.java @@ -0,0 +1,276 @@ +package cn.classfun.droidvm.lib.store.vm; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import androidx.annotation.StringRes; + +import cn.classfun.droidvm.R; +import cn.classfun.droidvm.lib.store.base.DataItem; +import cn.classfun.droidvm.lib.store.enums.Enums; +import cn.classfun.droidvm.lib.store.enums.StringEnum; + +/** + * Wrapper over a VM config's "boot" object — the single source of truth + * for how the guest is brought up: + * + *

+ * boot: {
+ *   protocol: "uefi" | "linux",
+ *   uefi:  { firmware },                       // empty = builtin EDK2
+ *   linux: {
+ *     source: "manual" | "image",
+ *     kernel, initrd, cmdline,                 // manual-source fields
+ *     image: {
+ *       disk,                                  // index into "disks"
+ *       entry: { id, title, version } | null,  // null = bootloader default
+ *       cmdline,                               // override, empty = from image
+ *       vdafix,                                // default true
+ *       wait                                   // boot menu seconds, 0 = off
+ *     }
+ *   }
+ * }
+ * 
+ * + * Both branches are kept in the store at once; {@code protocol} (and + * {@code source} below it) only select which one takes effect, so toggling + * in the UI never loses the other branch's values. Legacy flat keys + * ({@code use_uefi}/{@code kernel}/{@code initrd}/{@code cmdline}/ + * {@code bios}) are folded into this object once, on config load — see + * {@link VMConfig#migrateBoot}. + */ +public final class BootConfig { + public enum Protocol implements StringEnum { + UEFI(R.string.edit_vm_boot_protocol_uefi), + LINUX(R.string.edit_vm_boot_protocol_linux); + + private final @StringRes int stringId; + + Protocol(@StringRes int stringId) { + this.stringId = stringId; + } + + @Override + @StringRes + public int getStringId() { + return stringId; + } + } + + public enum LinuxSource implements StringEnum { + IMAGE(R.string.edit_vm_kernel_source_image), + MANUAL(R.string.edit_vm_kernel_source_manual); + + private final @StringRes int stringId; + + LinuxSource(@StringRes int stringId) { + this.stringId = stringId; + } + + @Override + @StringRes + public int getStringId() { + return stringId; + } + } + + public static final long DEFAULT_BOOT_WAIT = 1; + + /** Manual-source cmdline seed: first virtio disk, second partition. */ + public static final String DEFAULT_MANUAL_CMDLINE = "root=/dev/vda2"; + + /** + * Reserved {@code vm_start} boot_entry / boot-menu selection key for + * "boot this disk with DroidVM's built-in kernel" — not a disk image + * entry, so it is matched as a sentinel (see {@code BootPlan.resolve}) + * and can never collide with an lbx entry id (which never start '@'). + */ + public static final String BUILTIN_ENTRY_KEY = "@droidvm:builtin-kernel"; + + public final DataItem item; + + private BootConfig(@NonNull DataItem item) { + this.item = item; + } + + /** The "boot" object of {@code config}, created empty when missing. */ + @NonNull + public static BootConfig of(@NonNull VMConfig config) { + return new BootConfig(child(config.item, "boot")); + } + + /** Child object at {@code key}, created (and stored) when missing. */ + @NonNull + private static DataItem child(@NonNull DataItem parent, @NonNull String key) { + var c = parent.opt(key, null); + if (c == null || !c.is(DataItem.Type.OBJECT)) { + // set() stores a copy; re-read so callers mutate the stored item + parent.set(key, DataItem.newObject()); + c = parent.get(key); + } + return c; + } + + @NonNull + public Protocol getProtocol() { + return Enums.optEnum(item, "protocol", Protocol.UEFI); + } + + public void setProtocol(@NonNull Protocol protocol) { + item.set("protocol", protocol); + } + + // --- uefi branch --- + + @NonNull + private DataItem uefi() { + return child(item, "uefi"); + } + + /** Custom firmware path; empty = builtin EDK2 (QEMU backend only). */ + @NonNull + public String getUefiFirmware() { + return uefi().optString("firmware", ""); + } + + public void setUefiFirmware(@NonNull String firmware) { + uefi().set("firmware", firmware); + } + + // --- linux branch --- + + @NonNull + private DataItem linux() { + return child(item, "linux"); + } + + @NonNull + public LinuxSource getLinuxSource() { + return Enums.optEnum(linux(), "source", LinuxSource.MANUAL); + } + + public void setLinuxSource(@NonNull LinuxSource source) { + linux().set("source", source); + } + + @NonNull + public String getKernel() { + return linux().optString("kernel", ""); + } + + public void setKernel(@NonNull String kernel) { + linux().set("kernel", kernel); + } + + @NonNull + public String getInitrd() { + return linux().optString("initrd", ""); + } + + public void setInitrd(@NonNull String initrd) { + linux().set("initrd", initrd); + } + + /** Manual-source cmdline (image mode uses {@link #getImageCmdline}). */ + @NonNull + public String getCmdline() { + return linux().optString("cmdline", ""); + } + + public void setCmdline(@NonNull String cmdline) { + linux().set("cmdline", cmdline); + } + + // --- linux.image branch --- + + @NonNull + private DataItem image() { + return child(linux(), "image"); + } + + /** Index into the VM's "disks" array of the image to scan. */ + public int getImageDisk() { + return (int) image().optLong("disk", 0); + } + + public void setImageDisk(int disk) { + image().set("disk", (long) disk); + } + + /** Pinned boot entry; null = follow the bootloader default. */ + @Nullable + public ImageEntry getImageEntry() { + var e = image().opt("entry", null); + if (e == null || !e.is(DataItem.Type.OBJECT)) return null; + return new ImageEntry( + e.optString("id", null), + e.optString("title", null), + e.optString("version", null) + ); + } + + public void setImageEntry(@Nullable ImageEntry entry) { + if (entry == null) { + image().remove("entry"); + return; + } + var e = DataItem.newObject(); + e.set("id", entry.id); + e.set("title", entry.title); + e.set("version", entry.version); + image().set("entry", e); + } + + /** Image mode cmdline override; empty = use the image's cmdline. */ + @NonNull + public String getImageCmdline() { + return image().optString("cmdline", ""); + } + + public void setImageCmdline(@NonNull String cmdline) { + image().set("cmdline", cmdline); + } + + /** Rewrite root=/resume= device names to PARTUUID= (lbx vdafix). */ + public boolean isVdafix() { + return image().optBoolean("vdafix", true); + } + + public void setVdafix(boolean vdafix) { + image().set("vdafix", vdafix); + } + + /** Boot menu countdown in seconds on manual GUI start; 0 = no menu. */ + public long getBootWait() { + return image().optLong("wait", DEFAULT_BOOT_WAIT); + } + + public void setBootWait(long seconds) { + image().set("wait", Math.max(seconds, 0)); + } + + /** + * Identity of a pinned boot entry, matched against a fresh scan in + * order: exact id, exact title, then bootloader default ({@code lbx} + * ids embed the kernel version for BLS/extlinux, so updates in the + * guest are expected to miss here and fall back). + */ + public static final class ImageEntry { + @Nullable + public final String id; + @Nullable + public final String title; + @Nullable + public final String version; + + public ImageEntry( + @Nullable String id, + @Nullable String title, + @Nullable String version + ) { + this.id = id; + this.title = title; + this.version = version; + } + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMConfig.java b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMConfig.java index 103d52e4d..4aa00cde5 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMConfig.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMConfig.java @@ -24,6 +24,35 @@ public VMConfig(@NonNull JSONObject obj) throws JSONException { item.set("use_uefi", true); item.remove("kernel"); } + migrateBoot(); + } + + /** + * Folds the legacy flat boot keys (use_uefi/kernel/initrd/cmdline/bios) + * into the "boot" object on load. The legacy keys are left in place so + * the config file still boots under an older daemon/APK; everything in + * this codebase reads only the "boot" object from here on. + */ + private void migrateBoot() { + if (item.opt("boot", null) != null) return; + var legacy = item.opt("use_uefi", null) != null + || !item.optString("kernel", "").isEmpty() + || !item.optString("initrd", "").isEmpty() + || !item.optString("cmdline", "").isEmpty() + || !item.optString("bios", "").isEmpty(); + if (!legacy) return; + var boot = BootConfig.of(this); + var uefi = item.optBoolean("use_uefi", true); + // legacy QEMU oddity: use_uefi=false + "bios" + no kernel meant + // "boot a custom firmware" — that is UEFI protocol in the new model + if (!uefi && item.optString("kernel", "").isEmpty() + && !item.optString("bios", "").isEmpty()) + uefi = true; + boot.setProtocol(uefi ? BootConfig.Protocol.UEFI : BootConfig.Protocol.LINUX); + boot.setUefiFirmware(item.optString("bios", "")); + boot.setKernel(item.optString("kernel", "")); + boot.setInitrd(item.optString("initrd", "")); + boot.setCmdline(item.optString("cmdline", "")); } /** Iterates this VM's NIC entries (the "networks" array). */ diff --git a/app/src/main/java/cn/classfun/droidvm/ui/agent/base/AgentVM.java b/app/src/main/java/cn/classfun/droidvm/ui/agent/base/AgentVM.java index 94862a03f..42d5d28de 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/agent/base/AgentVM.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/agent/base/AgentVM.java @@ -28,6 +28,7 @@ import cn.classfun.droidvm.lib.store.disk.DiskBus; import cn.classfun.droidvm.lib.store.disk.DiskConfig; import cn.classfun.droidvm.lib.store.disk.DiskStore; +import cn.classfun.droidvm.lib.store.vm.BootConfig; import cn.classfun.droidvm.lib.store.vm.SharedDirType; import cn.classfun.droidvm.lib.store.vm.VMConfig; import cn.classfun.droidvm.lib.utils.FileUtils; @@ -108,10 +109,12 @@ public VMConfig buildVM() { vm.item.set("cpu_count", 1); vm.item.set("memory_mb", 384); vm.item.set("prepare_lend_mthp", false); - vm.item.set("use_uefi", false); - vm.item.set("kernel", PATH_BUILTIN_KERNEL); - vm.item.set("initrd", PATH_BUILTIN_INITRD); - vm.item.set("cmdline", "rd.systemd.unit=host-agent.target"); + var boot = BootConfig.of(vm); + boot.setProtocol(BootConfig.Protocol.LINUX); + boot.setLinuxSource(BootConfig.LinuxSource.MANUAL); + boot.setKernel(PATH_BUILTIN_KERNEL); + boot.setInitrd(PATH_BUILTIN_INITRD); + boot.setCmdline("rd.systemd.unit=host-agent.target"); var diskItems = DataItem.newArray(); for (var disk : disks) { var item = DataItem.newObject(); diff --git a/app/src/main/java/cn/classfun/droidvm/ui/disk/download/ImportURLActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/disk/download/ImportURLActivity.java index 8b4037c49..f313b1dca 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/disk/download/ImportURLActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/disk/download/ImportURLActivity.java @@ -49,6 +49,7 @@ import cn.classfun.droidvm.ui.disk.create.DiskFormat; import cn.classfun.droidvm.ui.widgets.tools.DownloadWidget; import cn.classfun.droidvm.ui.widgets.tools.DownloadWidget.OnDownloadListener; +import cn.classfun.droidvm.ui.widgets.tools.KernelAnalysisWidget; import cn.classfun.droidvm.ui.widgets.row.TextInputRowWidget; public final class ImportURLActivity extends AppCompatActivity implements OnDownloadListener { @@ -61,6 +62,7 @@ public final class ImportURLActivity extends AppCompatActivity implements OnDown private MaterialCardView cardInfo; private TextView tvInfoSize, tvInfoType; private DownloadWidget downloadWidget; + private KernelAnalysisWidget kernelAnalysis; private NestedScrollView scrollView; private ExtendedFloatingActionButton fabImport; private CollapsingToolbarLayout collapsingToolbar; @@ -88,6 +90,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { tvInfoSize = findViewById(R.id.tv_info_size); tvInfoType = findViewById(R.id.tv_info_type); downloadWidget = findViewById(R.id.download_widget); + kernelAnalysis = findViewById(R.id.kernel_analysis); scrollView = findViewById(R.id.scroll_view); fabImport = findViewById(R.id.fab_import); initialize(); @@ -111,6 +114,8 @@ public void handleOnBackPressed() { inputUrl.addTextChangedListener(new SimpleTextWatcher() { @Override public void onTextChanged(CharSequence s, int st, int b, int c) { + // The URL changed, so any loaded info / kernel analysis is stale. + kernelAnalysis.reset(); if (infoLoaded) { infoLoaded = false; cardInfo.setVisibility(GONE); @@ -118,6 +123,7 @@ public void onTextChanged(CharSequence s, int st, int b, int c) { } } }); + kernelAnalysis.setUrlProvider(() -> inputUrl.getText()); btnLoad.setOnClickListener(v -> doLoad()); fabImport.setOnClickListener(v -> doImport()); setStatusIdle(); diff --git a/app/src/main/java/cn/classfun/droidvm/ui/disk/lxc/ImportLxcImagesActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/disk/lxc/ImportLxcImagesActivity.java index 33e6f2481..b6cec1d6c 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/disk/lxc/ImportLxcImagesActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/disk/lxc/ImportLxcImagesActivity.java @@ -62,6 +62,7 @@ import cn.classfun.droidvm.ui.widgets.row.TextInputRowWidget; import cn.classfun.droidvm.ui.widgets.tools.DownloadWidget; import cn.classfun.droidvm.ui.widgets.tools.DownloadWidget.OnDownloadListener; +import cn.classfun.droidvm.ui.widgets.tools.KernelAnalysisWidget; public final class ImportLxcImagesActivity extends AppCompatActivity implements OnDownloadListener { private static final String TAG = "ImportLxcImages"; @@ -87,6 +88,7 @@ public final class ImportLxcImagesActivity extends AppCompatActivity implements private TextView tvInfoSize, tvInfoPath; private ExtendedFloatingActionButton fabImport; private DownloadWidget downloadWidget; + private KernelAnalysisWidget kernelAnalysis; private NestedScrollView scrollView; private CollapsingToolbarLayout collapsingToolbar; private MaterialToolbar toolbar; @@ -128,6 +130,9 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { tvInfoPath = findViewById(R.id.tv_info_path); fabImport = findViewById(R.id.fab_import); downloadWidget = findViewById(R.id.download_widget); + kernelAnalysis = findViewById(R.id.kernel_analysis); + kernelAnalysis.setUrlProvider(() -> selectedImage == null + ? null : pathJoin(getDownloadBaseUrl(), selectedImage.getDownloadPath())); scrollView = findViewById(R.id.scroll_view); initialize(); } @@ -469,6 +474,9 @@ private void showOutput(@NonNull LxcImage img) { var downloadUrl = pathJoin(getDownloadBaseUrl(), img.getDownloadPath()); tvInfoPath.setText(getString(R.string.lxc_info_path, downloadUrl)); cardInfo.setVisibility(VISIBLE); + // A new image is selected: offer a fresh kernel analysis for it. + kernelAnalysis.setVisibility(VISIBLE); + kernelAnalysis.reset(); } private void hideOutput() { @@ -623,6 +631,7 @@ private void setOutputEnabled(boolean enabled) { tvInfoSize.setText(""); tvInfoPath.setText(""); cardInfo.setVisibility(GONE); + kernelAnalysis.setVisibility(GONE); selectedImage = null; } } diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/VMActions.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/VMActions.java index 006db820d..7c7d5d0b0 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/VMActions.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/VMActions.java @@ -1,9 +1,12 @@ package cn.classfun.droidvm.ui.vm; import static android.widget.Toast.LENGTH_LONG; +import static cn.classfun.droidvm.lib.Constants.PATH_BUILTIN_INITRD; +import static cn.classfun.droidvm.lib.Constants.PATH_BUILTIN_KERNEL; import static cn.classfun.droidvm.ui.main.settings.MainSettingsFragment.isAutoConsoleEnabled; import static cn.classfun.droidvm.ui.main.settings.MainSettingsFragment.isClearLogsBeforeStartEnabled; +import android.content.Context; import android.os.Handler; import android.util.Log; import android.widget.Toast; @@ -16,8 +19,11 @@ import cn.classfun.droidvm.R; import cn.classfun.droidvm.lib.daemon.DaemonConnection; +import cn.classfun.droidvm.lib.store.vm.BootConfig; import cn.classfun.droidvm.lib.store.vm.VMConfig; +import cn.classfun.droidvm.lib.store.vm.VMStore; import cn.classfun.droidvm.lib.ui.UIContext; +import cn.classfun.droidvm.ui.vm.boot.BootMenuDialog; public final class VMActions { private static final String TAG = "VMActions"; @@ -30,6 +36,84 @@ public static void createAndStart( @NonNull Handler mainHandler, @NonNull UIContext ui, @NonNull AtomicBoolean wantOpenConsole + ) { + // manual GUI start of an image-booting VM goes through the + // GRUB-style entry menu first (auto-start in the daemon does not) + if (BootMenuDialog.wanted(config) && ui.isAlive()) { + BootMenuDialog.show( + ui.getContext(), config, + (bootEntry, remember, selected, builtinCmdline) -> { + var startEntry = remember + ? rememberChoice(ui.getContext(), config, bootEntry, + selected, builtinCmdline) + : bootEntry; + doCreateAndStart(config, mainHandler, ui, wantOpenConsole, startEntry); + }, + () -> { /* cancelled: do not start */ } + ); + return; + } + doCreateAndStart(config, mainHandler, ui, wantOpenConsole, null); + } + + /** + * Persists a "remember this choice" boot-menu selection to the on-disk + * VM store — the source of truth the list reloads on resume, which is + * why a one-shot start that only mutated the in-memory config never + * stuck. The same change is mirrored onto {@code config} so the daemon + * gets it via vm_modify. The built-in kernel is baked into a plain + * manual source (so it persists without a one-shot override and the + * menu stops appearing for that VM); image entries are pinned, with a + * null selection clearing the pin to follow the bootloader default. + * + * @return the boot_entry to start with (null once the built-in choice + * has been baked into the config; the original key otherwise) + */ + @Nullable + private static String rememberChoice( + @NonNull Context context, + @NonNull VMConfig config, + @Nullable String bootEntry, + @Nullable BootConfig.ImageEntry selected, + @Nullable String builtinCmdline + ) { + boolean builtin = BootConfig.BUILTIN_ENTRY_KEY.equals(bootEntry); + applyChoice(BootConfig.of(config), builtin, selected, builtinCmdline); + var store = new VMStore(); + if (store.load(context)) { + var stored = store.findById(config.getId()); + if (stored != null) { + applyChoice(BootConfig.of(stored), builtin, selected, builtinCmdline); + store.save(context); + } + } + // built-in now boots as a plain manual config — no one-shot override + return builtin ? null : bootEntry; + } + + private static void applyChoice( + @NonNull BootConfig boot, + boolean builtin, + @Nullable BootConfig.ImageEntry selected, + @Nullable String builtinCmdline + ) { + if (!builtin) { + boot.setImageEntry(selected); + return; + } + boot.setLinuxSource(BootConfig.LinuxSource.MANUAL); + boot.setKernel(PATH_BUILTIN_KERNEL); + boot.setInitrd(PATH_BUILTIN_INITRD); + boot.setCmdline(builtinCmdline != null && !builtinCmdline.isEmpty() + ? builtinCmdline : BootConfig.DEFAULT_MANUAL_CMDLINE); + } + + private static void doCreateAndStart( + @NonNull VMConfig config, + @NonNull Handler mainHandler, + @NonNull UIContext ui, + @NonNull AtomicBoolean wantOpenConsole, + @Nullable String bootEntry ) { var conn = DaemonConnection.getInstance(); var createReq = conn.buildRequest("vm_exists"); @@ -46,6 +130,7 @@ public static void createAndStart( .buildRequest("vm_start") .copy(resp, "vm_id") .put("clear_logs_before_start", isClearLogsBeforeStartEnabled(ui.getContext())) + .put("boot_entry", bootEntry == null ? "" : bootEntry) .onResponse(onStart) .onUnsuccessful(f) .onError(err) diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/boot/BootEntries.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/boot/BootEntries.java new file mode 100644 index 000000000..85066a8d5 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/boot/BootEntries.java @@ -0,0 +1,301 @@ +package cn.classfun.droidvm.ui.vm.boot; + +import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import cn.classfun.droidvm.R; +import cn.classfun.droidvm.lib.daemon.DaemonConnection; +import cn.classfun.droidvm.lib.store.vm.BootConfig; + +/** + * UI-side view of an image's boot entries: scans via the daemon's + * {@code vm_bootscan} (disk images are usually only readable by the + * daemon process) and mirrors {@code BootPlan}'s pin-matching rules so + * the edit tab and the boot menu preview exactly what would boot. + */ +public final class BootEntries { + @NonNull + public final List entries; + + private BootEntries(@NonNull List entries) { + this.entries = entries; + } + + public interface OnScan { + /** Called on the daemon executor thread; post to main as needed. */ + void onScan(@Nullable BootEntries result, @Nullable String error); + } + + /** Live download progress from lbx's {@code progress:} lines. */ + public interface OnProgress { + /** Chunks downloaded so far, and the file's total (-1 if unknown). */ + void onProgress(int loaded, int total); + } + + /** One entry of `lbx entries --json`. */ + public static final class Entry { + @Nullable + public final String id; + @Nullable + public final String title; + @Nullable + public final String version; + public final boolean isDefault; + @NonNull + public final String source; + @NonNull + public final String kernel; + @NonNull + public final List initrd; + @NonNull + public final String cmdline; + @NonNull + public final String cmdlineFixed; + /** CONFIG_DMA_RESTRICTED_POOL: TRUE / FALSE, or null when lbx + * could not determine it (no config-* file in the image). */ + @Nullable + public final Boolean dmaRestrictedPool; + + Entry(@NonNull JSONObject o) { + id = emptyToNull(optStr(o, "id")); + title = emptyToNull(optStr(o, "title")); + version = emptyToNull(optStr(o, "version")); + isDefault = o.optBoolean("default", false); + source = optStr(o, "source"); + kernel = optStr(o, "kernel"); + cmdline = optStr(o, "cmdline"); + cmdlineFixed = optStr(o, "cmdline_fixed"); + dmaRestrictedPool = o.isNull("dma_restricted_pool") + ? null : o.optBoolean("dma_restricted_pool", false); + initrd = new ArrayList<>(); + var arr = o.optJSONArray("initrd"); + if (arr != null) for (int i = 0; i < arr.length(); i++) { + var s = arr.optString(i, ""); + if (!s.isEmpty()) initrd.add(s); + } + } + + @NonNull + public String label() { + if (title != null) return title; + if (version != null) return version; + if (id != null) return id; + return "(untitled)"; + } + + @NonNull + public String displayLabel(@NonNull Context ctx) { + return isDefault + ? ctx.getString(R.string.edit_vm_boot_entry_default_mark, label()) + : label(); + } + + /** The selection key sent as the start request's boot_entry. */ + @NonNull + public String selectionKey() { + return id != null ? id : label(); + } + + @NonNull + public BootConfig.ImageEntry toImageEntry() { + return new BootConfig.ImageEntry(id, title, version); + } + + /** The cmdline that would boot, honoring the vdafix switch. */ + @NonNull + public String effectiveCmdline(boolean vdafix) { + return vdafix && !cmdlineFixed.isEmpty() ? cmdlineFixed : cmdline; + } + + /** vdafix did not (or could not) neutralize a device-name root=. */ + public boolean hasRootDeviceWarning(boolean vdafix) { + var c = effectiveCmdline(vdafix); + return c.contains("root=/dev/sd") || c.contains("root=/dev/nvme") + || c.contains("root=/dev/hd"); + } + + /** + * Kernel is known to lack CONFIG_DMA_RESTRICTED_POOL, so its virtio + * DMA cannot reach the host-shared pool and the devices fail under a + * gunyah protected VM. Unknown (null) does not warn. + */ + public boolean lacksRestrictedDmaPool() { + return Boolean.FALSE.equals(dmaRestrictedPool); + } + } + + @Nullable + private static String emptyToNull(@NonNull String s) { + return s.isEmpty() ? null : s; + } + + /** + * Null-safe string read: org.json's optString turns a JSON null into + * the literal string "null" instead of the fallback. + */ + @NonNull + static String optStr(@NonNull JSONObject o, @NonNull String key) { + return o.isNull(key) ? "" : o.optString(key, ""); + } + + /** Asynchronous scan of {@code image} through the daemon. */ + public static void scan(@NonNull String image, @NonNull OnScan callback) { + DaemonConnection.getInstance().buildRequest("vm_bootscan") + .put("image", image) + .onResponse(resp -> callback.onScan( + parse(resp.optJSONArray("entries")), null)) + .onUnsuccessful(resp -> callback.onScan( + null, resp.optString("message", "scan failed"))) + .onError(e -> callback.onScan(null, fmt("%s", e.getMessage()))) + .invoke(); + } + + /** Hard cap in case lbx wedges; its own --timeout handles normal stalls. */ + private static final long ANALYZE_HARD_TIMEOUT_MS = 5 * 60_000; + + /** + * Analyze a remote (URL) image by running lbx directly from the UI — a URL + * fetch needs no root, and lbx ships as {@code liblbx.so} in the app's + * nativeLibraryDir, which the app may execute (unlike the daemon's + * data-dir copy). So we skip the daemon and stream lbx's + * {@code progress: /} lines for a live count instead of a spinner. + * Runs on a background thread; both callbacks fire there, so post to main. + * {@code timeoutSecs} becomes lbx's per-connection {@code --timeout}, so a + * stalled chunk download aborts on its own rather than hanging the UI. + */ + public static void analyzeUrl( + @NonNull Context ctx, @NonNull String url, int timeoutSecs, + @NonNull OnProgress onProgress, @NonNull OnScan onScan + ) { + var lbx = ctx.getApplicationInfo().nativeLibraryDir + "/liblbx.so"; + var thread = new Thread( + () -> runAnalyze(lbx, url, timeoutSecs, onProgress, onScan), "lbx-analyze"); + thread.setDaemon(true); + thread.start(); + } + + private static void runAnalyze( + @NonNull String lbx, @NonNull String url, int timeoutSecs, + @NonNull OnProgress onProgress, @NonNull OnScan onScan + ) { + Process process = null; + try { + process = new ProcessBuilder( + lbx, "--timeout", String.valueOf(timeoutSecs), "entries", url, "--json" + ).start(); + var proc = process; + // Drain stdout (the JSON) on its own thread so the pipe never + // blocks while we read stderr line by line. + var stdout = new ByteArrayOutputStream(); + var stdoutThread = new Thread(() -> { + try { + proc.getInputStream().transferTo(stdout); + } catch (IOException ignored) { + } + }); + stdoutThread.start(); + var errBuf = new StringBuilder(); + try (var br = new BufferedReader(new InputStreamReader( + proc.getErrorStream(), StandardCharsets.UTF_8))) { + String line; + while ((line = br.readLine()) != null) { + if (!reportProgress(line, onProgress)) + errBuf.append(line).append('\n'); + } + } + stdoutThread.join(); + if (!proc.waitFor(ANALYZE_HARD_TIMEOUT_MS, TimeUnit.MILLISECONDS)) { + proc.destroyForcibly(); + onScan.onScan(null, "analysis timed out"); + return; + } + int code = proc.exitValue(); + if (code == 0) { + onScan.onScan(parse(new JSONArray(stdout.toString("UTF-8"))), null); + } else { + // Surface lbx's real stderr rather than a generic error. + var msg = errBuf.toString().trim(); + onScan.onScan(null, msg.isEmpty() ? fmt("lbx exited with %d", code) : msg); + } + } catch (Exception e) { + onScan.onScan(null, e.getMessage() != null ? e.getMessage() : "analysis failed"); + } finally { + if (process != null) process.destroy(); + } + } + + /** Parse a {@code progress: /} line, reporting it; false otherwise. */ + private static boolean reportProgress(@NonNull String line, @NonNull OnProgress cb) { + var t = line.trim(); + if (!t.startsWith("progress:")) return false; + var rest = t.substring("progress:".length()).trim(); + int slash = rest.indexOf('/'); + try { + int loaded = Integer.parseInt((slash < 0 ? rest : rest.substring(0, slash)).trim()); + int total = slash < 0 ? -1 : Integer.parseInt(rest.substring(slash + 1).trim()); + cb.onProgress(loaded, total); + } catch (NumberFormatException ignored) { + return false; // not actually a progress line + } + return true; + } + + @NonNull + private static BootEntries parse(@Nullable JSONArray arr) { + var list = new ArrayList(); + if (arr != null) for (int i = 0; i < arr.length(); i++) { + var o = arr.optJSONObject(i); + if (o != null) list.add(new Entry(o)); + } + return new BootEntries(list); + } + + /** + * The entry a boot would use right now: pinned entry by exact id, + * then exact title, falling back to the bootloader default — the + * same cascade as the daemon's BootPlan. + */ + @Nullable + public Entry resolve(@Nullable BootConfig.ImageEntry pinned) { + if (pinned != null) { + for (var e : entries) + if (pinned.id != null && pinned.id.equals(e.id)) return e; + for (var e : entries) + if (pinned.title != null && pinned.title.equals(e.title)) return e; + } + return defaultEntry(); + } + + /** True when {@code pinned} no longer matches any entry. */ + public boolean isFallback(@Nullable BootConfig.ImageEntry pinned) { + if (pinned == null) return false; + for (var e : entries) { + if (pinned.id != null && pinned.id.equals(e.id)) return false; + if (pinned.title != null && pinned.title.equals(e.title)) return false; + } + return true; + } + + @Nullable + public Entry defaultEntry() { + for (var e : entries) + if (e.isDefault) return e; + return entries.isEmpty() ? null : entries.get(0); + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/boot/BootMenuDialog.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/boot/BootMenuDialog.java new file mode 100644 index 000000000..68bbf8940 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/boot/BootMenuDialog.java @@ -0,0 +1,369 @@ +package cn.classfun.droidvm.ui.vm.boot; + +import static android.view.View.GONE; +import static android.view.View.VISIBLE; +import static cn.classfun.droidvm.lib.store.enums.Enums.optEnum; + +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.text.Spannable; +import android.text.SpannableStringBuilder; +import android.text.style.ForegroundColorSpan; +import android.view.LayoutInflater; +import android.widget.CheckBox; +import android.widget.RadioButton; +import android.widget.RadioGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; + +import com.google.android.material.color.MaterialColors; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +import cn.classfun.droidvm.R; +import cn.classfun.droidvm.lib.store.vm.BootConfig; +import cn.classfun.droidvm.lib.store.vm.ProtectedVM; +import cn.classfun.droidvm.lib.store.vm.VMConfig; + +/** + * GRUB-style boot entry menu shown on a manual GUI start of a VM that + * boots from a disk image: the resolved entry is preselected, a countdown + * runs on the Boot button (touching the list stops it, like pressing a + * key in GRUB), and the choice is one-shot unless "remember" is ticked. + */ +public final class BootMenuDialog { + /** RadioButton id of the fixed "DroidVM built-in kernel" entry, kept + * well clear of the per-index ids ({@link #idFor}). */ + private static final int BUILTIN_RADIO_ID = 1_000_000; + + /** Extra grace on the auto-boot countdown when the entry it would boot + * carries a warning, giving the user time to pick another. */ + private static final int WARNED_COUNTDOWN_GRACE = 3; + + public interface OnProceed { + /** + * @param bootEntry one-shot entry selection key for vm_start + * (null = follow stored config; + * {@link BootConfig#BUILTIN_ENTRY_KEY} = built-in kernel) + * @param remember whether to persist the selection (auto persists + * as "clear pin"; built-in switches to a manual config) + * @param selected image entry to pin when remembering (null otherwise) + * @param builtinCmdline the cmdline the built-in kernel would use, for + * persisting; non-null only for the built-in entry + */ + void proceed(@Nullable String bootEntry, boolean remember, + @Nullable BootConfig.ImageEntry selected, + @Nullable String builtinCmdline); + } + + private BootMenuDialog() { + } + + /** True when this VM's config wants the menu on manual start. */ + public static boolean wanted(@NonNull VMConfig config) { + var boot = BootConfig.of(config); + return boot.getProtocol() == BootConfig.Protocol.LINUX + && boot.getLinuxSource() == BootConfig.LinuxSource.IMAGE + && boot.getBootWait() > 0; + } + + public static void show( + @NonNull Context context, + @NonNull VMConfig config, + @NonNull OnProceed onProceed, + @NonNull Runnable onCancel + ) { + var boot = BootConfig.of(config); + var view = LayoutInflater.from(context) + .inflate(R.layout.dialog_boot_menu, null); + RadioGroup group = view.findViewById(R.id.rg_boot_entries); + TextView summary = view.findViewById(R.id.tv_boot_menu_summary); + TextView warn = view.findViewById(R.id.tv_boot_menu_warn); + CheckBox remember = view.findViewById(R.id.cb_boot_menu_remember); + + var state = new Object() { + @Nullable + BootEntries entries; + long secondsLeft = Math.max(boot.getBootWait(), 1); + boolean countdownActive = false; + boolean done = false; + }; + var handler = new Handler(Looper.getMainLooper()); + + var dialog = new MaterialAlertDialogBuilder(context) + .setTitle(context.getString( + R.string.edit_vm_boot_menu_title, config.getName())) + .setView(view) + .setPositiveButton(R.string.edit_vm_boot_menu_boot, null) + .setNegativeButton(android.R.string.cancel, null) + .setCancelable(false) + .create(); + + Runnable fire = () -> { + if (state.done) return; + state.done = true; + dialog.dismiss(); + var entries = state.entries; + int checkedId = group.getCheckedRadioButtonId(); + if (checkedId == BUILTIN_RADIO_ID) { + onProceed.proceed(BootConfig.BUILTIN_ENTRY_KEY, remember.isChecked(), + null, builtinCmdline(boot, entries)); + return; + } + // radio index 0 = auto; entry i sits at radio index i + 1 + int index = checkedId - 1; + if (entries == null || index <= 0) { + onProceed.proceed(null, remember.isChecked(), null, null); + return; + } + var entry = entries.entries.get(index - 1); + onProceed.proceed( + entry.selectionKey(), remember.isChecked(), entry.toImageEntry(), null); + }; + + var tick = new Runnable() { + @Override + public void run() { + if (state.done || !state.countdownActive) return; + var btn = dialog.getButton(AlertDialog.BUTTON_POSITIVE); + if (state.secondsLeft <= 0) { + fire.run(); + return; + } + btn.setText(context.getString( + R.string.edit_vm_boot_menu_boot_in, state.secondsLeft)); + state.secondsLeft--; + handler.postDelayed(this, 1000); + } + }; + + Runnable stopCountdown = () -> { + if (!state.countdownActive) return; + state.countdownActive = false; + dialog.getButton(AlertDialog.BUTTON_POSITIVE) + .setText(R.string.edit_vm_boot_menu_boot); + }; + + // fixed escape hatch pinned to the bottom of the list: boot the disk + // with DroidVM's built-in kernel (useful when the guest's own kernel + // won't direct-boot, or its bootloader can't be scanned) + Runnable addBuiltin = () -> addRadio(context, group, BUILTIN_RADIO_ID, + context.getString(R.string.edit_vm_boot_entry_builtin), null, stopCountdown); + + dialog.setOnShowListener(d -> { + dialog.getButton(AlertDialog.BUTTON_POSITIVE) + .setOnClickListener(v -> fire.run()); + dialog.getButton(AlertDialog.BUTTON_NEGATIVE) + .setOnClickListener(v -> { + state.done = true; + dialog.dismiss(); + onCancel.run(); + }); + }); + dialog.show(); + + // auto option is always present; entries arrive after the scan, and + // the built-in entry is appended last so it stays at the bottom + addRadio(context, group, idFor(0), + context.getString(R.string.edit_vm_boot_entry_auto), null, stopCountdown); + group.check(idFor(0)); + summary.setVisibility(VISIBLE); + summary.setText(R.string.edit_vm_boot_detect_scanning); + + var image = firstImagePath(config, boot.getImageDisk()); + if (image == null) { + summary.setText(R.string.edit_vm_boot_detect_no_disk); + addBuiltin.run(); + return; + } + BootEntries.scan(image, (result, error) -> handler.post(() -> { + if (state.done) return; + if (result == null) { + summary.setText(context.getString( + R.string.edit_vm_boot_detect_failed, error)); + addBuiltin.run(); + return; + } + state.entries = result; + var pinned = boot.getImageEntry(); + var resolved = result.resolve(pinned); + boolean protectedVm = isProtectedVm(config); + for (int i = 0; i < result.entries.size(); i++) { + var entry = result.entries.get(i); + addRadio(context, group, idFor(i + 1), entry.displayLabel(context), + entryWarning(context, protectedVm, entry), stopCountdown); + } + addBuiltin.run(); + // the entry "Auto" would boot is whatever the stored config + // resolves to now; surface its warning on the Auto line too + var autoWarn = resolved != null + ? entryWarning(context, protectedVm, resolved) : null; + var autoRadio = (RadioButton) group.findViewById(idFor(0)); + if (autoRadio != null) + autoRadio.setText(warnedLabel(autoRadio, + context.getString(R.string.edit_vm_boot_entry_auto), autoWarn)); + if (resolved != null && pinned != null && !result.isFallback(pinned)) + group.check(idFor(result.entries.indexOf(resolved) + 1)); + else + group.check(idFor(0)); + if (result.isFallback(pinned)) { + warn.setVisibility(VISIBLE); + warn.setText(context.getString( + R.string.edit_vm_boot_menu_fallback, + pinned.title != null ? pinned.title : pinned.id)); + } + updateSummary(context, summary, boot, result, group); + group.setOnCheckedChangeListener((g, id) -> + updateSummary(context, summary, boot, result, group)); + // countdown only starts on a successful scan; a warned auto-boot + // target buys the user a few extra seconds to react + if (autoWarn != null) state.secondsLeft += WARNED_COUNTDOWN_GRACE; + state.countdownActive = true; + tick.run(); + })); + } + + private static void updateSummary( + @NonNull Context context, + @NonNull TextView summary, + @NonNull BootConfig boot, + @NonNull BootEntries entries, + @NonNull RadioGroup group + ) { + int checkedId = group.getCheckedRadioButtonId(); + if (checkedId == BUILTIN_RADIO_ID) { + var line = context.getString(R.string.edit_vm_boot_entry_builtin); + var cmdline = builtinCmdline(boot, entries); + if (!cmdline.isEmpty()) line += "\n" + cmdline; + summary.setVisibility(VISIBLE); + summary.setText(line); + return; + } + int index = checkedId - 1; + var entry = index <= 0 + ? entries.resolve(null) + : entries.entries.get(index - 1); + if (entry == null) { + summary.setVisibility(GONE); + return; + } + var line = entry.kernel + " · " + entry.source; + var cmdline = entry.effectiveCmdline(boot.isVdafix()); + if (!cmdline.isEmpty()) line += "\n" + cmdline; + summary.setVisibility(VISIBLE); + summary.setText(line); + } + + /** + * The cmdline the built-in kernel would boot with, mirroring + * {@code BootPlan.builtinCmdline}: the image cmdline override if set, + * else the resolved entry's effective (vdafix-adjusted) cmdline so the + * right {@code root=} comes up, else the manual default. + */ + @NonNull + private static String builtinCmdline( + @NonNull BootConfig boot, @Nullable BootEntries entries) { + var override = boot.getImageCmdline(); + if (!override.isEmpty()) return override; + if (entries != null) { + var entry = entries.resolve(boot.getImageEntry()); + if (entry != null) { + var cmdline = entry.effectiveCmdline(boot.isVdafix()); + if (!cmdline.isEmpty()) return cmdline; + } + } + return BootConfig.DEFAULT_MANUAL_CMDLINE; + } + + private static int idFor(int index) { + // RadioButton ids must be positive; 0 is reserved/invalid + return index + 1; + } + + private static void addRadio( + @NonNull Context context, + @NonNull RadioGroup group, + int id, + @NonNull String label, + @Nullable String warning, + @NonNull Runnable onTouched + ) { + var rb = new RadioButton(context); + rb.setId(id); + rb.setText(warnedLabel(rb, label, warning)); + rb.setOnClickListener(v -> onTouched.run()); + group.addView(rb); + } + + /** + * The radio label, with {@code warning} appended as a red second line + * when present (the per-entry warning area). Plain {@code label} + * otherwise. + */ + @NonNull + private static CharSequence warnedLabel( + @NonNull TextView colorSource, + @NonNull String label, + @Nullable String warning + ) { + if (warning == null || warning.isEmpty()) return label; + var sb = new SpannableStringBuilder(label).append('\n').append(warning); + int color = MaterialColors.getColor( + colorSource, androidx.appcompat.R.attr.colorError); + sb.setSpan(new ForegroundColorSpan(color), + label.length() + 1, sb.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + return sb; + } + + /** + * Whether this VM boots as a gunyah protected VM (protected, or + * protected-without-firmware) — read from the stored config, where a + * guest kernel without CONFIG_DMA_RESTRICTED_POOL cannot drive virtio. + * Mirrors {@code VMEditBootTab.isProtectedVm}. + */ + private static boolean isProtectedVm(@NonNull VMConfig config) { + var pvm = optEnum(config.item, "protected_vm", + ProtectedVM.PROTECTED_WITHOUT_FIRMWARE); + return pvm == ProtectedVM.PROTECTED_PROTECTED + || pvm == ProtectedVM.PROTECTED_WITHOUT_FIRMWARE; + } + + /** + * Optional warning for a boot entry, or null when it has none. Today the + * only case is a kernel lbx flagged as lacking CONFIG_DMA_RESTRICTED_POOL + * on a protected VM (same condition as the edit tab's detection card), + * shown here as a terse one-liner since the menu is cramped — the edit + * tab keeps the full explanation. + */ + @Nullable + private static String entryWarning( + @NonNull Context context, + boolean protectedVm, + @NonNull BootEntries.Entry entry + ) { + if (protectedVm && entry.lacksRestrictedDmaPool()) + return context.getString(R.string.edit_vm_boot_entry_dma_warn); + return null; + } + + @Nullable + private static String firstImagePath(@NonNull VMConfig config, int preferred) { + var disks = config.item.opt("disks", null); + if (disks == null + || !disks.is(cn.classfun.droidvm.lib.store.base.DataItem.Type.ARRAY)) + return null; + var arr = disks.asArray(); + if (preferred >= 0 && preferred < arr.size()) { + var p = arr.get(preferred).optString("path", ""); + if (!p.isEmpty()) return p; + } + for (var d : arr) { + var p = d.optString("path", ""); + if (!p.isEmpty()) return p; + } + return null; + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/VMEditActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/VMEditActivity.java index 1f31a22ec..835b5f21e 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/VMEditActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/VMEditActivity.java @@ -93,6 +93,13 @@ public void onTabSwitchedByDrag(int newIndex) { currentTab = VMEditTab.fromIndex(newIndex); tabLayout.selectTab(tabLayout.getTabAt(newIndex)); scrollView.scrollTo(0, 0); + tabs.get(newIndex).onTabShown(); + } + + /** The live tab instance, e.g. to read another tab's unsaved state. */ + @NonNull + public VMEditBaseTab getTab(@NonNull VMEditTab tab) { + return tabs.get(tab.ordinal()); } private void setupTabs() { @@ -108,6 +115,7 @@ public void onTabSelected(TabLayout.Tab tab) { currentTab = VMEditTab.fromIndex(newPos); swipeHelper.animateToTab(newPos, direction); scrollView.scrollTo(0, 0); + tabs.get(newPos).onTabShown(); } @Override diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/base/VMEditBaseTab.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/base/VMEditBaseTab.java index 5c87760b1..ddc286eec 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/base/VMEditBaseTab.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/base/VMEditBaseTab.java @@ -21,6 +21,10 @@ public VMEditBaseTab(VMEditActivity parent, View view) { public abstract void initValue(); + /** Called when this tab becomes the visible one. */ + public void onTabShown() { + } + public abstract void loadConfig(@NonNull VMConfig config); public abstract boolean validateInput(@NonNull VMStore store); diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/basic/VMEditBasicTab.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/basic/VMEditBasicTab.java index 78c4ff313..ab7ff0923 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/basic/VMEditBasicTab.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/basic/VMEditBasicTab.java @@ -8,6 +8,7 @@ import android.view.View; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import cn.classfun.droidvm.R; import cn.classfun.droidvm.lib.store.vm.ProtectedVM; @@ -158,4 +159,14 @@ public void saveConfig(@NonNull VMConfig config) { VMBackend backend = chooseBackend.getSelectedItem(); item.set("backend", backend); } + + /** + * The protection mode as currently selected (before save), so other + * tabs can warn about protected-VM constraints without re-reading the + * stored config. + */ + @Nullable + public ProtectedVM getCurrentProtectedVm() { + return chooseProtectedVm.getSelectedItem(); + } } diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/boot/VMEditBootTab.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/boot/VMEditBootTab.java index ff082671c..75d6f7324 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/boot/VMEditBootTab.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/boot/VMEditBootTab.java @@ -7,37 +7,91 @@ import static cn.classfun.droidvm.lib.Constants.PATH_MICRODROID_INITRD; import static cn.classfun.droidvm.lib.Constants.PATH_MICRODROID_INITRD_DEBUG; import static cn.classfun.droidvm.lib.Constants.PATH_MICRODROID_KERNEL; +import static cn.classfun.droidvm.lib.store.enums.Enums.optEnum; +import static cn.classfun.droidvm.lib.store.vm.ProtectedVM.PROTECTED_WITHOUT_FIRMWARE; import static cn.classfun.droidvm.lib.utils.FileUtils.checkFilePath; import static cn.classfun.droidvm.lib.utils.FileUtils.shellAsyncCheckExists; import static cn.classfun.droidvm.lib.utils.StringUtils.resolveUriPath; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.style.ForegroundColorSpan; import android.view.MenuItem; import android.view.View; +import android.widget.EditText; +import android.widget.ImageButton; import android.widget.TextView; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.material.color.MaterialColors; import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.google.android.material.materialswitch.MaterialSwitch; + +import java.util.ArrayList; +import java.util.regex.Pattern; import cn.classfun.droidvm.R; +import cn.classfun.droidvm.lib.store.base.DataItem; +import cn.classfun.droidvm.lib.store.vm.BootConfig; +import cn.classfun.droidvm.lib.store.vm.ProtectedVM; import cn.classfun.droidvm.lib.store.vm.VMConfig; import cn.classfun.droidvm.lib.store.vm.VMStore; +import cn.classfun.droidvm.lib.ui.IconItemAdapter; import cn.classfun.droidvm.lib.ui.MenuDialogBuilder; +import cn.classfun.droidvm.ui.vm.boot.BootEntries; import cn.classfun.droidvm.ui.vm.edit.VMEditActivity; +import cn.classfun.droidvm.ui.vm.edit.basic.VMEditBasicTab; import cn.classfun.droidvm.ui.vm.edit.base.VMEditBaseTab; +import cn.classfun.droidvm.ui.vm.edit.base.VMEditTab; +import cn.classfun.droidvm.ui.vm.edit.storage.VMEditStorageTab; import cn.classfun.droidvm.ui.widgets.container.CollapsibleContainer; +import cn.classfun.droidvm.ui.widgets.row.DropdownRowWidget; import cn.classfun.droidvm.ui.widgets.row.SwitchRowWidget; import cn.classfun.droidvm.ui.widgets.row.TextInputRowWidget; public final class VMEditBootTab extends VMEditBaseTab { + /** Captures a root=/resume= param: group 1 = key, group 2 = value. */ + private static final Pattern ROOT_PARAM = + Pattern.compile("(?:^|\\s)(root|resume)=(\\S+)"); + private SwitchRowWidget swAutoUp; - private SwitchRowWidget swUefi; - private TextView tvUefiHint; + private DropdownRowWidget ddProtocol; + private CollapsibleContainer containerUefi; + private TextInputRowWidget inputUefiFirmware; private CollapsibleContainer containerKernel; + private DropdownRowWidget ddSource; + private View layoutImage; + private View layoutManual; + private DropdownRowWidget ddBootDisk; + private DropdownRowWidget ddBootEntry; + private ImageButton btnRescan; + private EditText etDetectKernel; + private EditText etDetectInitrd; + private EditText etDetectCmdline; + private EditText etDetectSource; + private TextView tvKernelDmaWarn; + private TextView tvDetectWarn; + private MaterialSwitch swVdafix; + private EditText etCmdlineOverride; + private TextInputRowWidget inputBootWait; private TextInputRowWidget inputKernel; private TextInputRowWidget inputInitrd; private TextInputRowWidget inputCmdline; + /** The config being edited; the source of the disks list for scans. */ + private VMConfig config; + private BootConfig.Protocol protocol = BootConfig.Protocol.UEFI; + private BootConfig.LinuxSource source = BootConfig.LinuxSource.MANUAL; + private int bootDiskIndex = 0; + @Nullable + private BootConfig.ImageEntry pinnedEntry; + @Nullable + private BootEntries scanned; + @Nullable + private String lastScannedImage; + public VMEditBootTab(VMEditActivity parent, View view) { super(parent, view); } @@ -45,9 +99,25 @@ public VMEditBootTab(VMEditActivity parent, View view) { @Override public void initView() { swAutoUp = view.findViewById(R.id.sw_auto_up); - swUefi = view.findViewById(R.id.sw_uefi); - tvUefiHint = view.findViewById(R.id.tv_uefi_hint); + ddProtocol = view.findViewById(R.id.dd_boot_protocol); + containerUefi = view.findViewById(R.id.container_uefi); + inputUefiFirmware = view.findViewById(R.id.input_uefi_firmware); containerKernel = view.findViewById(R.id.container_kernel); + ddSource = view.findViewById(R.id.dd_kernel_source); + layoutImage = view.findViewById(R.id.layout_kernel_image); + layoutManual = view.findViewById(R.id.layout_kernel_manual); + ddBootDisk = view.findViewById(R.id.dd_boot_disk); + ddBootEntry = view.findViewById(R.id.dd_boot_entry); + btnRescan = view.findViewById(R.id.btn_boot_rescan); + etDetectKernel = view.findViewById(R.id.et_detect_kernel); + etDetectInitrd = view.findViewById(R.id.et_detect_initrd); + etDetectCmdline = view.findViewById(R.id.et_detect_cmdline); + etDetectSource = view.findViewById(R.id.et_detect_source); + tvKernelDmaWarn = view.findViewById(R.id.tv_boot_kernel_dma_warn); + tvDetectWarn = view.findViewById(R.id.tv_boot_detect_warn); + swVdafix = view.findViewById(R.id.sw_vdafix); + etCmdlineOverride = view.findViewById(R.id.et_cmdline_override); + inputBootWait = view.findViewById(R.id.input_boot_wait); inputKernel = view.findViewById(R.id.input_kernel); inputInitrd = view.findViewById(R.id.input_initrd); inputCmdline = view.findViewById(R.id.input_cmdline); @@ -55,58 +125,434 @@ public void initView() { @Override public void initValue() { + ddProtocol.setAdapter(IconItemAdapter.create(parent, new String[]{ + BootConfig.Protocol.UEFI.getDisplayString(parent), + BootConfig.Protocol.LINUX.getDisplayString(parent), + }, new int[]{R.drawable.ic_uefi_nocolor, R.drawable.ic_linux})); + ddProtocol.setOnItemClickListener((p, v, pos, id) -> { + protocol = pos == 0 + ? BootConfig.Protocol.UEFI : BootConfig.Protocol.LINUX; + applyVisibility(); + }); + ddSource.setAdapter(IconItemAdapter.create(parent, new String[]{ + BootConfig.LinuxSource.IMAGE.getDisplayString(parent), + BootConfig.LinuxSource.MANUAL.getDisplayString(parent), + }, R.drawable.ic_linux)); + ddSource.setOnItemClickListener((p, v, pos, id) -> { + source = pos == 0 + ? BootConfig.LinuxSource.IMAGE : BootConfig.LinuxSource.MANUAL; + applyVisibility(); + if (isImageMode() && scanned == null) rescan(); + }); + inputUefiFirmware.setIconButtonOnClickListener(this::showFirmwareBrowseDialog); inputKernel.setIconButtonOnClickListener(this::showKernelBrowseDialog); inputInitrd.setIconButtonOnClickListener(this::showInitrdBrowseDialog); - swUefi.setOnCheckedChangeListener((btn, checked) -> setUefiMode(checked)); + btnRescan.setOnClickListener(v -> rescan()); + swVdafix.setOnCheckedChangeListener((btn, checked) -> updateDetectionCard()); + // create mode never calls loadConfig, so seed the defaults here: + // UEFI protocol; manual source prefilled with the builtin kernel + ddProtocol.setText(protocol.getDisplayString(parent)); + ddSource.setText(source.getDisplayString(parent)); + inputKernel.setTextAndMoveCursor(PATH_BUILTIN_KERNEL); + inputInitrd.setTextAndMoveCursor(PATH_BUILTIN_INITRD); + inputCmdline.setTextAndMoveCursor(BootConfig.DEFAULT_MANUAL_CMDLINE); + inputBootWait.setTextAndMoveCursor(String.valueOf(BootConfig.DEFAULT_BOOT_WAIT)); + applyVisibility(); + } + + @Override + public void onTabShown() { + if (!isImageMode()) return; + updateDiskDropdown(); + var image = scanImagePath(); + // disks added/changed in the storage tab since the last scan + if (image == null || !image.equals(lastScannedImage)) { + rescan(); + } else if (scanned != null) { + // same image, but the protection mode may have changed in the + // basic tab — refresh the detection card so its warning tracks it + updateDetectionCard(); + } + } + + private boolean isImageMode() { + return protocol == BootConfig.Protocol.LINUX + && source == BootConfig.LinuxSource.IMAGE; } - private void setUefiMode(boolean enabled) { - containerKernel.setVisibility(enabled ? GONE : VISIBLE); - tvUefiHint.setVisibility(enabled ? VISIBLE : GONE); + private void applyVisibility() { + boolean uefi = protocol == BootConfig.Protocol.UEFI; + containerUefi.setVisibility(uefi ? VISIBLE : GONE); + containerKernel.setVisibility(uefi ? GONE : VISIBLE); + boolean image = source == BootConfig.LinuxSource.IMAGE; + layoutImage.setVisibility(image ? VISIBLE : GONE); + layoutManual.setVisibility(image ? GONE : VISIBLE); } @Override public void loadConfig(@NonNull VMConfig config) { - var item = config.item; - var useUefi = item.optBoolean("use_uefi", true); - var kernel = item.optString("kernel", ""); - var initrd = item.optString("initrd", ""); - var cmdline = item.optString("cmdline", ""); - swUefi.setChecked(useUefi); - setUefiMode(useUefi); - inputKernel.setTextAndMoveCursor(kernel); - inputInitrd.setTextAndMoveCursor(initrd); - inputCmdline.setTextAndMoveCursor(cmdline); - swAutoUp.setChecked(item.optBoolean("auto_up", false)); + this.config = config; + var boot = BootConfig.of(config); + protocol = boot.getProtocol(); + source = boot.getLinuxSource(); + ddProtocol.setText(protocol.getDisplayString(parent)); + ddSource.setText(source.getDisplayString(parent)); + inputUefiFirmware.setTextAndMoveCursor(boot.getUefiFirmware()); + inputKernel.setTextAndMoveCursor(boot.getKernel()); + inputInitrd.setTextAndMoveCursor(boot.getInitrd()); + bootDiskIndex = boot.getImageDisk(); + pinnedEntry = boot.getImageEntry(); + swVdafix.setChecked(boot.isVdafix()); + inputBootWait.setTextAndMoveCursor(String.valueOf(boot.getBootWait())); + inputCmdline.setTextAndMoveCursor(boot.getCmdline()); + etCmdlineOverride.setText(boot.getImageCmdline()); + swAutoUp.setChecked(config.item.optBoolean("auto_up", false)); + updateDiskDropdown(); + updateEntryDropdown(); + applyVisibility(); + if (isImageMode()) rescan(); } @Override public boolean validateInput(@NonNull VMStore store) { - if (!swUefi.isChecked()) { - var kernel = inputKernel.getText(); - var initrd = inputInitrd.getText(); - inputKernel.setError(null); - inputInitrd.setError(null); - if (!checkFilePath(kernel, true)) { - inputKernel.setError(parent.getString(R.string.create_vm_error_path_invalid)); + if (protocol == BootConfig.Protocol.UEFI) { + var firmware = inputUefiFirmware.getText(); + inputUefiFirmware.setError(null); + if (!firmware.isEmpty() && !checkFilePath(firmware, true)) { + inputUefiFirmware.setError(parent.getString(R.string.create_vm_error_path_invalid)); return false; } - if (!initrd.isEmpty() && !checkFilePath(initrd, true)) { - inputInitrd.setError(parent.getString(R.string.create_vm_error_path_invalid)); + return true; + } + if (source == BootConfig.LinuxSource.IMAGE) { + inputBootWait.setError(null); + if (scanImagePath() == null) { + showDetectError(parent.getString(R.string.edit_vm_boot_detect_no_disk)); return false; } + try { + var wait = inputBootWait.getText().trim(); + if (!wait.isEmpty()) Long.parseLong(wait); + } catch (NumberFormatException e) { + inputBootWait.setError(parent.getString(R.string.edit_vm_boot_wait_invalid)); + return false; + } + return true; + } + var kernel = inputKernel.getText(); + var initrd = inputInitrd.getText(); + inputKernel.setError(null); + inputInitrd.setError(null); + if (!checkFilePath(kernel, true)) { + inputKernel.setError(parent.getString(R.string.create_vm_error_path_invalid)); + return false; + } + if (!initrd.isEmpty() && !checkFilePath(initrd, true)) { + inputInitrd.setError(parent.getString(R.string.create_vm_error_path_invalid)); + return false; } return true; } @Override public void saveConfig(@NonNull VMConfig config) { - var item = config.item; - item.set("use_uefi", swUefi.isChecked()); - item.set("kernel", inputKernel.getText()); - item.set("initrd", inputInitrd.getText()); - item.set("cmdline", inputCmdline.getText()); - item.set("auto_up", swAutoUp.isChecked()); + var boot = BootConfig.of(config); + boot.setProtocol(protocol); + boot.setUefiFirmware(inputUefiFirmware.getText()); + boot.setLinuxSource(source); + boot.setKernel(inputKernel.getText()); + boot.setInitrd(inputInitrd.getText()); + // separate fields: switching source must not clobber the other's + boot.setCmdline(inputCmdline.getText()); + boot.setImageCmdline(etCmdlineOverride.getText().toString()); + boot.setImageDisk(bootDiskIndex); + boot.setImageEntry(pinnedEntry); + boot.setVdafix(swVdafix.isChecked()); + var wait = inputBootWait.getText().trim(); + try { + boot.setBootWait(wait.isEmpty() + ? BootConfig.DEFAULT_BOOT_WAIT : Long.parseLong(wait)); + } catch (NumberFormatException ignored) { + } + config.item.set("auto_up", swAutoUp.isChecked()); + } + + // --- image source: disk/entry dropdowns + detection card --- + + /** + * Disk paths as currently edited in the storage tab (a disk added + * there must be visible here before the config is saved), falling + * back to the loaded config. + */ + @NonNull + private ArrayList diskPaths() { + var out = new ArrayList(); + DataItem disks = null; + try { + var storage = (VMEditStorageTab) parent.getTab(VMEditTab.TAB_STORAGE); + disks = storage.getCurrentDisks(); + } catch (Exception ignored) { + } + if (disks == null && config != null) + disks = config.item.opt("disks", null); + if (disks == null || !disks.is(DataItem.Type.ARRAY)) return out; + for (var disk : disks.asArray()) + out.add(disk.optString("path", "")); + return out; + } + + @Nullable + private String diskPath(int index) { + var paths = diskPaths(); + if (index < 0 || index >= paths.size()) return null; + var p = paths.get(index); + return p.isEmpty() ? null : p; + } + + @Nullable + private String firstDiskPath() { + var paths = diskPaths(); + for (int i = 0; i < paths.size(); i++) + if (!paths.get(i).isEmpty()) return paths.get(i); + return null; + } + + @Nullable + private String scanImagePath() { + var p = diskPath(bootDiskIndex); + return p != null ? p : firstDiskPath(); + } + + @NonNull + private static String basename(@NonNull String path) { + return path.substring(path.lastIndexOf('/') + 1); + } + + private void updateDiskDropdown() { + var paths = diskPaths(); + var labels = new ArrayList(); + var indexes = new ArrayList(); + for (int i = 0; i < paths.size(); i++) { + if (paths.get(i).isEmpty()) continue; + labels.add(basename(paths.get(i))); + indexes.add(i); + } + ddBootDisk.setAdapter(IconItemAdapter.create( + parent, labels, R.drawable.ic_nav_disk)); + ddBootDisk.setOnItemClickListener((p, v, pos, id) -> { + bootDiskIndex = indexes.get(pos); + rescan(); + }); + var current = scanImagePath(); + ddBootDisk.setText(current == null + ? parent.getString(R.string.edit_vm_boot_detect_no_disk) + : basename(current)); + } + + private void updateEntryDropdown() { + var labels = new ArrayList(); + labels.add(parent.getString(R.string.edit_vm_boot_entry_auto)); + var entries = scanned; + if (entries != null) + for (var e : entries.entries) + labels.add(e.displayLabel(parent)); + ddBootEntry.setAdapter(IconItemAdapter.create( + parent, labels, R.drawable.ic_linux)); + ddBootEntry.setOnItemClickListener((p, v, pos, id) -> { + pinnedEntry = pos == 0 || entries == null + ? null : entries.entries.get(pos - 1).toImageEntry(); + updateDetectionCard(); + }); + // the selected label: pinned entry when it still exists, else auto + var label = parent.getString(R.string.edit_vm_boot_entry_auto); + if (pinnedEntry != null) { + if (entries != null && !entries.isFallback(pinnedEntry)) { + var resolved = entries.resolve(pinnedEntry); + if (resolved != null) label = resolved.displayLabel(parent); + } else if (pinnedEntry.title != null) { + label = pinnedEntry.title; + } else if (pinnedEntry.id != null) { + label = pinnedEntry.id; + } + } + ddBootEntry.setText(label); + } + + private void rescan() { + scanned = null; + var image = scanImagePath(); + lastScannedImage = image; + updateDiskDropdown(); + updateEntryDropdown(); + if (image == null) { + showDetectError(parent.getString(R.string.edit_vm_boot_detect_no_disk)); + return; + } + setDetectFields("", "", null, ""); + etDetectKernel.setText(R.string.edit_vm_boot_detect_scanning); + tvKernelDmaWarn.setVisibility(GONE); + tvDetectWarn.setVisibility(GONE); + BootEntries.scan(image, (result, error) -> parent.runOnUiThread(() -> { + if (parent.isFinishing()) return; + scanned = result; + if (result == null) { + showDetectError(parent.getString( + R.string.edit_vm_boot_detect_failed, error)); + } else { + updateEntryDropdown(); + updateDetectionCard(); + } + })); + } + + private void showDetectError(@NonNull String message) { + setDetectFields("", "", null, ""); + tvKernelDmaWarn.setVisibility(GONE); + tvDetectWarn.setText(message); + tvDetectWarn.setVisibility(VISIBLE); + } + + private void setDetectFields( + @NonNull String kernel, + @NonNull String initrd, + @Nullable CharSequence cmdline, + @NonNull String source + ) { + etDetectKernel.setText(kernel); + etDetectInitrd.setText(initrd); + etDetectCmdline.setText(cmdline); + etDetectSource.setText(source); + } + + private void updateDetectionCard() { + var entries = scanned; + if (entries == null) return; + var entry = entries.resolve(pinnedEntry); + if (entry == null) { + showDetectError(parent.getString( + R.string.edit_vm_boot_detect_failed, "no entries")); + return; + } + setDetectFields( + entry.kernel, + String.join(", ", entry.initrd), + highlightRootParams(entry, swVdafix.isChecked()), + entry.source + ); + // protected-VM DMA warning sits directly under the kernel field + if (isProtectedVm() && entry.lacksRestrictedDmaPool()) { + tvKernelDmaWarn.setText(R.string.edit_vm_boot_protected_dma_warn); + tvKernelDmaWarn.setVisibility(VISIBLE); + } else { + tvKernelDmaWarn.setVisibility(GONE); + } + if (entries.isFallback(pinnedEntry)) { + tvDetectWarn.setText(parent.getString( + R.string.edit_vm_boot_menu_fallback, pinnedLabel())); + tvDetectWarn.setVisibility(VISIBLE); + } else { + tvDetectWarn.setVisibility(GONE); + } + } + + /** + * Whether the VM would boot as a gunyah protected VM (protected, or + * protected-without-firmware) — read live from the basic tab so a + * just-changed selection is reflected, then the stored config, then + * the backend default. In such a VM a guest kernel without + * CONFIG_DMA_RESTRICTED_POOL cannot drive virtio. + */ + private boolean isProtectedVm() { + ProtectedVM pvm = null; + try { + var basic = (VMEditBasicTab) parent.getTab(VMEditTab.TAB_BASIC); + pvm = basic.getCurrentProtectedVm(); + } catch (Exception ignored) { + } + if (pvm == null && config != null) + pvm = optEnum(config.item, "protected_vm", PROTECTED_WITHOUT_FIRMWARE); + if (pvm == null) pvm = PROTECTED_WITHOUT_FIRMWARE; + return pvm == ProtectedVM.PROTECTED_PROTECTED + || pvm == ProtectedVM.PROTECTED_WITHOUT_FIRMWARE; + } + + /** + * Colors a root=/resume= value red to flag the vdafix rewrite. With vdafix + * off we show the raw cmdline and mark the /dev/sdX device that is about to + * be replaced (a PARTUUID root is fine and stays unmarked). With vdafix on + * we show the fixed cmdline and mark only the values the fix actually + * changed, so an already-PARTUUID root (and any unrelated param) is left + * uncolored. + */ + @NonNull + private CharSequence highlightRootParams(@NonNull BootEntries.Entry entry, boolean vdafix) { + var shown = entry.effectiveCmdline(vdafix); + if (shown.isEmpty()) return shown; + var span = new SpannableString(shown); + var color = MaterialColors.getColor( + etDetectCmdline, androidx.appcompat.R.attr.colorError); + var m = ROOT_PARAM.matcher(shown); + while (m.find()) { + var key = m.group(1); + var value = m.group(2); + if (key == null || value == null) continue; + boolean mark; + if (!vdafix) { + // off: the device path that the fix would replace + mark = value.startsWith("/dev/"); + } else { + // on: only what the fix changed for this key + var original = paramValue(entry.cmdline, key); + mark = original != null && !original.equals(value); + } + if (mark) span.setSpan( + new ForegroundColorSpan(color), + m.start(2), m.end(2), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ); + } + return span; + } + + /** The value of {@code key}=… in {@code cmdline}, or null if absent. */ + @Nullable + private static String paramValue(@NonNull String cmdline, @NonNull String key) { + var m = ROOT_PARAM.matcher(cmdline); + while (m.find()) + if (key.equals(m.group(1))) return m.group(2); + return null; + } + + @NonNull + private String pinnedLabel() { + if (pinnedEntry == null) return ""; + if (pinnedEntry.title != null) return pinnedEntry.title; + return pinnedEntry.id != null ? pinnedEntry.id : ""; + } + + // --- browse menus --- + + private void showFirmwareBrowseDialog() { + MenuItem.OnMenuItemClickListener listener = item -> { + var id = item.getItemId(); + if (id == R.id.menu_firmware_builtin) { + inputUefiFirmware.setText(""); + } else if (id == R.id.menu_firmware_external) { + parent.currentPicker = uri -> { + if (uri != null) + inputUefiFirmware.setTextAndMoveCursor(resolveUriPath(parent, uri)); + parent.currentPicker = null; + }; + parent.filePickerLauncher.launch(new String[]{"*/*"}); + } + return true; + }; + MenuDialogBuilder.showSimple( + parent, + R.string.edit_vm_uefi_firmware_browse_title, + R.menu.menu_vm_firmware_browse, + listener + ); } private void showKernelBrowseDialog() { diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/storage/VMEditStorageTab.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/storage/VMEditStorageTab.java index 5ad4c77b6..b6d2c6572 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/storage/VMEditStorageTab.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/storage/VMEditStorageTab.java @@ -112,6 +112,12 @@ private void activityResult(@NonNull ActivityResult result) { pendingBrowsePosition = -1; } + /** Live disk entries as edited right now (not yet saved to config). */ + @NonNull + public DataItem getCurrentDisks() { + return listDisks.getItems(); + } + @Override public void loadConfig(@NonNull VMConfig config) { listDisks.setItems(config.item.opt("disks", DataItem.newArray())); diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/info/VMInfoActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/info/VMInfoActivity.java index d3af1bd62..0bc38a88e 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/info/VMInfoActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/info/VMInfoActivity.java @@ -21,6 +21,7 @@ import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; @@ -45,6 +46,7 @@ import cn.classfun.droidvm.lib.daemon.DaemonConnection; import cn.classfun.droidvm.lib.daemon.ForegroundCallback; import cn.classfun.droidvm.lib.store.base.DataItem; +import cn.classfun.droidvm.lib.store.vm.BootConfig; import cn.classfun.droidvm.lib.store.vm.ProtectedVM; import cn.classfun.droidvm.lib.store.vm.VMConfig; import cn.classfun.droidvm.lib.store.vm.VMState; @@ -204,12 +206,27 @@ private void reloadConfig() { }); } + @NonNull + private String bootSummary() { + var boot = BootConfig.of(config); + if (boot.getProtocol() == BootConfig.Protocol.UEFI) + return getString(R.string.edit_vm_boot_protocol_uefi); + if (boot.getLinuxSource() == BootConfig.LinuxSource.IMAGE) { + var pinned = boot.getImageEntry(); + var label = getString(R.string.edit_vm_kernel_source_image); + if (pinned != null && pinned.title != null) + return label + " · " + pinned.title; + return label; + } + return basename(boot.getKernel()); + } + private void populateInfo() { var item = config.item; collapsingToolbar.setTitle(config.getName()); rowCpu.setValue(getString(R.string.vm_info_cpu_value, item.optLong("cpu_count", 0))); rowMemory.setValue(getString(R.string.vm_info_memory_value, item.optLong("memory_mb", 0))); - rowKernel.setValue(basename(item.optString("kernel", ""))); + rowKernel.setValue(bootSummary()); var protectedVm = optEnum(item, "protected_vm", ProtectedVM.PROTECTED_WITHOUT_FIRMWARE); rowProtected.setValue(protectedVm.getDisplayString(this)); var disks = item.opt("disks", DataItem.newArray()); diff --git a/app/src/main/java/cn/classfun/droidvm/ui/widgets/tools/KernelAnalysisWidget.java b/app/src/main/java/cn/classfun/droidvm/ui/widgets/tools/KernelAnalysisWidget.java new file mode 100644 index 000000000..93b261411 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/ui/widgets/tools/KernelAnalysisWidget.java @@ -0,0 +1,192 @@ +package cn.classfun.droidvm.ui.widgets.tools; + +import static android.view.View.GONE; +import static android.view.View.VISIBLE; + +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.google.android.material.button.MaterialButton; +import com.google.android.material.color.MaterialColors; +import com.google.android.material.progressindicator.CircularProgressIndicator; +import com.google.android.material.textfield.TextInputEditText; + +import java.util.ArrayList; + +import cn.classfun.droidvm.R; +import cn.classfun.droidvm.lib.ui.IconItemAdapter; +import cn.classfun.droidvm.ui.vm.boot.BootEntries; +import cn.classfun.droidvm.ui.widgets.row.DropdownRowWidget; + +/** + * A drop-in "kernel analysis" card for the import screens. The user picks an + * image (a URL); pressing Analyze runs a single cloud analysis + * ({@code lbx entries --json}, via the daemon's {@code vm_bootscan}) + * and the card shows, per boot entry, the kernel / initrd / cmdline and the + * protected-VM DMA warning — the same read-only view the VM edit screen + * gives a local disk image. + * + *

One analysis is one lbx call that returns every entry with every field, + * so there's nothing to cache. The host wires a {@link UrlProvider} (where to + * read the current URL) and calls {@link #reset()} whenever that URL changes. + */ +public final class KernelAnalysisWidget extends FrameLayout { + + /** Supplies the http(s) URL to analyse, or {@code null} if none yet. */ + public interface UrlProvider { + @Nullable + String getUrl(); + } + + /** lbx per-connection timeout: a stalled chunk download aborts this fast. */ + private static final int CHUNK_TIMEOUT_SECS = 8; + + private final Handler main = new Handler(Looper.getMainLooper()); + + private MaterialButton btnAnalyze; + private CircularProgressIndicator progress; + private TextView tvStatus; + private TextView tvDmaWarn; + private View groupResults; + private DropdownRowWidget ddEntry; + private TextInputEditText etKernel; + private TextInputEditText etInitrd; + private TextInputEditText etCmdline; + + @Nullable + private UrlProvider urlProvider; + @Nullable + private BootEntries scanned; + private boolean scanning = false; + + public KernelAnalysisWidget(@NonNull Context context) { + super(context); + init(); + } + + public KernelAnalysisWidget(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + init(); + } + + private void init() { + LayoutInflater.from(getContext()).inflate(R.layout.view_kernel_analysis, this, true); + btnAnalyze = findViewById(R.id.btn_analyze); + progress = findViewById(R.id.progress_analysis); + tvStatus = findViewById(R.id.tv_analysis_status); + tvDmaWarn = findViewById(R.id.tv_analysis_dma_warn); + groupResults = findViewById(R.id.group_analysis_results); + ddEntry = findViewById(R.id.dd_analysis_entry); + etKernel = findViewById(R.id.et_analysis_kernel); + etInitrd = findViewById(R.id.et_analysis_initrd); + etCmdline = findViewById(R.id.et_analysis_cmdline); + btnAnalyze.setOnClickListener(v -> analyze()); + } + + public void setUrlProvider(@Nullable UrlProvider provider) { + this.urlProvider = provider; + } + + /** Clear any shown result; call when the source URL/selection changes. */ + public void reset() { + if (scanning) return; + scanned = null; + groupResults.setVisibility(GONE); + tvDmaWarn.setVisibility(GONE); + setStatus(getContext().getString(R.string.kernel_analysis_hint), false); + } + + private void analyze() { + if (scanning) return; + var url = urlProvider != null ? urlProvider.getUrl() : null; + url = url != null ? url.trim() : ""; + if (!url.startsWith("http://") && !url.startsWith("https://")) { + setStatus(getContext().getString(R.string.kernel_analysis_need_url), true); + return; + } + scanning = true; + progress.setVisibility(VISIBLE); + btnAnalyze.setEnabled(false); + groupResults.setVisibility(GONE); + tvDmaWarn.setVisibility(GONE); + setStatus(getContext().getString(R.string.edit_vm_boot_detect_scanning), false); + BootEntries.analyzeUrl(getContext(), url, CHUNK_TIMEOUT_SECS, + (loaded, total) -> main.post(() -> onProgress(loaded, total)), + (result, error) -> main.post(() -> onScan(result, error))); + } + + private void onProgress(int loaded, int total) { + if (!scanning) return; + setStatus(total > 0 + ? getContext().getString(R.string.kernel_analysis_progress, loaded, total) + : getContext().getString(R.string.kernel_analysis_progress_n, loaded), false); + } + + private void onScan(@Nullable BootEntries result, @Nullable String error) { + scanning = false; + progress.setVisibility(GONE); + btnAnalyze.setEnabled(true); + if (result == null || result.entries.isEmpty()) { + scanned = null; + groupResults.setVisibility(GONE); + tvDmaWarn.setVisibility(GONE); + setStatus(getContext().getString( + R.string.edit_vm_boot_detect_failed, + error != null ? error : "no entries"), true); + return; + } + scanned = result; + var labels = new ArrayList(); + for (var e : result.entries) labels.add(e.displayLabel(getContext())); + ddEntry.setAdapter(IconItemAdapter.create(getContext(), labels, R.drawable.ic_linux)); + ddEntry.setOnItemClickListener((p, v, pos, id) -> showEntry(pos)); + int def = defaultIndex(result); + ddEntry.setText(labels.get(def)); + groupResults.setVisibility(VISIBLE); + setStatus(getContext().getString( + R.string.kernel_analysis_found, result.entries.size()), false); + showEntry(def); + } + + private static int defaultIndex(@NonNull BootEntries entries) { + for (int i = 0; i < entries.entries.size(); i++) + if (entries.entries.get(i).isDefault) return i; + return 0; + } + + private void showEntry(int pos) { + if (scanned == null || pos < 0 || pos >= scanned.entries.size()) return; + var e = scanned.entries.get(pos); + etKernel.setText(e.kernel); + etInitrd.setText(String.join(", ", e.initrd)); + // vdafix is applied on a real boot, so preview the bootable cmdline. + // No root= highlighting here: with no vdafix toggle to contrast + // against, the rewrite markup would have nothing to mean. + etCmdline.setText(e.effectiveCmdline(true)); + // Imported images become protected VMs, so warn unconditionally when + // the kernel can't do restricted-pool DMA (same text as the editor). + if (e.lacksRestrictedDmaPool()) { + tvDmaWarn.setText(R.string.edit_vm_boot_protected_dma_warn); + tvDmaWarn.setVisibility(VISIBLE); + } else { + tvDmaWarn.setVisibility(GONE); + } + } + + private void setStatus(@NonNull String message, boolean error) { + tvStatus.setText(message); + tvStatus.setTextColor(MaterialColors.getColor(this, error + ? androidx.appcompat.R.attr.colorError + : com.google.android.material.R.attr.colorOnSurfaceVariant)); + tvStatus.setVisibility(VISIBLE); + } +} diff --git a/app/src/main/res/layout/activity_import_lxc_images.xml b/app/src/main/res/layout/activity_import_lxc_images.xml index c839811b8..06f51f6ab 100644 --- a/app/src/main/res/layout/activity_import_lxc_images.xml +++ b/app/src/main/res/layout/activity_import_lxc_images.xml @@ -170,6 +170,15 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/partial_vm_edit_boot.xml b/app/src/main/res/layout/partial_vm_edit_boot.xml index 079f3e028..cf6bad958 100644 --- a/app/src/main/res/layout/partial_vm_edit_boot.xml +++ b/app/src/main/res/layout/partial_vm_edit_boot.xml @@ -1,6 +1,7 @@ @@ -12,24 +13,41 @@ android:icon="@drawable/ic_power" android:text="@string/network_edit_auto_up" /> - + android:hint="@string/edit_vm_boot_protocol" + android:icon="@drawable/ic_uefi_nocolor" /> - + android:title="@string/edit_vm_boot_protocol_uefi"> + + + + + + - + android:hint="@string/edit_vm_kernel_source" + android:icon="@drawable/ic_linux" /> - + android:orientation="vertical" + android:visibility="gone"> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + android:orientation="vertical"> + + + + + + + + diff --git a/app/src/main/res/layout/view_kernel_analysis.xml b/app/src/main/res/layout/view_kernel_analysis.xml new file mode 100644 index 000000000..f87f6c20b --- /dev/null +++ b/app/src/main/res/layout/view_kernel_analysis.xml @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/menu/menu_vm_firmware_browse.xml b/app/src/main/res/menu/menu_vm_firmware_browse.xml new file mode 100644 index 000000000..fc4e4f84b --- /dev/null +++ b/app/src/main/res/menu/menu_vm_firmware_browse.xml @@ -0,0 +1,14 @@ + +

+ + + + + + diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index a9a07475c..d1dcb16cd 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -3,4 +3,6 @@ #FF1A3825 #FF382D1A #FF3A1B1B + + #FF64B5F6 diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 87df285b1..650cb176b 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -273,7 +273,56 @@ 选择外部文件 目标内核不存在: %1$s 使用 UEFI 启动 + 引导方式 + UEFI + Linux 开机协议 + UEFI 固件(留空 = 内置 EDK2) + 内置 EDK2 + 选择 UEFI 固件 + 自定义固件仅对 QEMU 后端生效\ncrosvm 始终使用内置 EDK2\nUEFI 引导 Linux 适配中,尚不可用;如需引导 Linux,请选择 Linux 内核引导方式 + 内核来源 + 磁盘镜像 + 手动指定 + 引导磁盘 + 选择引导磁盘 + 引导项 + 自动(跟随引导加载器) + 客户机更新内核后自动生效 + %1$s(默认) + DroidVM 内置 Linux 内核 + 缺失 DMA_RESTRICTED_POOL + 检测到的引导文件 + 引导项 + 内核 + initrd + 内核命令行 + 来源 + 正在扫描… + 扫描失败:%1$s + 此虚拟机尚未配置磁盘镜像 + root= 指向物理设备;请启用自动修正或覆盖 cmdline + 重新扫描 + 自动修正 root= 设备 + 将 root=/dev/sdX 重写为 PARTUUID 以适配 virtio + 开机菜单等待秒数 + 0 = 不显示菜单直接启动 + 必须为秒数(数字) + 内核命令行覆盖(留空 = 使用镜像内的) + 留空 = 使用镜像内的 cmdline + 选择引导项 — %1$s + 启动 + 启动 (%1$d) + 记住此选择 + 未找到固定的引导项“%1$s”,已改用默认 + 此内核未启用 CONFIG_DMA_RESTRICTED_POOL。\n在受保护 VM 下客户机无法与主机共享 DMA 内存,virtio 设备(磁盘/网络)将无法启动。\n请改用内置内核,或重新编译并启用该选项。 如果想要直接启动 Linux 内核则请禁用 UEFI + 内核分析 + 分析 + 导入前通过网络分析镜像的内核/initrd + 请先输入有效的 http(s):// 镜像 URL + 找到 %1$d 个引导项 + 下载中:%1$d/%2$d 块 + 下载中:%1$d 块 选择 initramfs 清空 DroidVM 内置 Linux initrd diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index a73a57d2d..0f5cfb0fc 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -20,4 +20,6 @@ #FFDFFAE4 #FFFFF9E0 #FFFADFE0 + + #FF1976D2 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d973861c7..2faf73b8c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -275,7 +275,56 @@ Select an external file Target kernel is not exists: %1$s Boot with UEFI + Boot protocol + UEFI + Linux boot protocol + UEFI firmware (empty = built-in EDK2) + Built-in EDK2 + Select UEFI firmware + Custom firmware only takes effect on the QEMU backend\ncrosvm always uses the built-in EDK2\nBooting Linux via UEFI is still being adapted and is not usable yet — to boot Linux, choose the Linux kernel boot protocol + Kernel source + Disk image + Manual + Boot disk + Select boot disk + Boot entry + Auto (bootloader default) + Follows guest kernel updates automatically + %1$s (default) + DroidVM built-in Linux kernel + Missing DMA_RESTRICTED_POOL + Detected boot files + entry + Kernel + initrd + Kernel cmdline + Source + Scanning… + Scan failed: %1$s + No disk image configured for this VM + root= names a physical device; enable auto-fix or override the cmdline + Rescan + Auto-fix root= device + Rewrites root=/dev/sdX to PARTUUID for virtio + Boot menu countdown (seconds) + 0 = boot immediately without menu + Must be a number of seconds + Kernel cmdline override (empty = from image) + Empty = use cmdline from the image + Select boot entry — %1$s + Boot + Boot (%1$d) + Remember this choice + Pinned entry "%1$s" not found; using default + This kernel lacks CONFIG_DMA_RESTRICTED_POOL.\nUnder a protected VM the guest cannot share DMA memory with the host, so virtio devices (disk/network) will fail to start.\nUse the built-in kernel, or rebuild with CONFIG_DMA_RESTRICTED_POOL. To boot a Linux kernel directly, please disable UEFI + Kernel analysis + Analyze + Analyze the image\'s kernel/initrd over the network before importing + Enter a valid http(s):// image URL first + %1$d boot entries found + Downloading: %1$d/%2$d chunks + Downloading: %1$d chunks Select initramfs Clear DroidVM built-in Linux initrd From 87c46084153a191eea497dedb9a6bd37696df481 Mon Sep 17 00:00:00 2001 From: HuJK Date: Sun, 14 Jun 2026 16:38:07 +0800 Subject: [PATCH 011/182] feat(download): keep disk imports running in background Move disk image downloads out of the import activities into a foreground service backed by a self-written resumable downloader, with notification progress, retry/pause state, and one active import at a time. Rewire URL, flat image, and LXC import screens to attach to the service state while keeping DownloadWidget display-only. Co-authored-by: Claude Opus 4.8 --- app/src/main/AndroidManifest.xml | 7 + .../lib/download/DiskDownloadManager.java | 467 ++++++++++++++++++ .../lib/download/DiskDownloadService.java | 326 ++++++++++++ .../ui/disk/download/ImportURLActivity.java | 213 ++++++-- .../ui/disk/images/ImportImagesActivity.java | 203 ++++++-- .../ui/disk/lxc/ImportLxcImagesActivity.java | 243 +++++++-- .../ui/widgets/tools/DownloadWidget.java | 226 +++------ app/src/main/res/values-zh-rCN/strings.xml | 19 + app/src/main/res/values/strings.xml | 19 + 9 files changed, 1409 insertions(+), 314 deletions(-) create mode 100644 app/src/main/java/cn/classfun/droidvm/lib/download/DiskDownloadManager.java create mode 100644 app/src/main/java/cn/classfun/droidvm/lib/download/DiskDownloadService.java diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ffe9d0410..be017c56a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -8,6 +8,8 @@ + + + + diff --git a/app/src/main/java/cn/classfun/droidvm/lib/download/DiskDownloadManager.java b/app/src/main/java/cn/classfun/droidvm/lib/download/DiskDownloadManager.java new file mode 100644 index 000000000..1bc6dc6c7 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/lib/download/DiskDownloadManager.java @@ -0,0 +1,467 @@ +package cn.classfun.droidvm.lib.download; + +import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; + +import android.content.Context; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.ArrayList; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; + +import cn.classfun.droidvm.R; +import cn.classfun.droidvm.lib.store.disk.DiskConfig; +import cn.classfun.droidvm.lib.store.disk.DiskStore; + +/** + * Self-written disk-image download engine. We stream the file ourselves with + * {@link HttpURLConnection} (following redirects, resuming via HTTP Range, and + * retrying on read timeouts / dropped connections) instead of the system + * DownloadManager, whose OEM variants either reject app-private destinations or + * time out aggressively without resuming on this CDN — while plain `curl` works. + * + *

Because the app holds MANAGE_EXTERNAL_STORAGE we write straight to the + * user's folder (a {@code .part} file, renamed on completion). State lives in an + * in-memory registry; {@link DiskDownloadService} runs {@link #runDownload} on a + * background thread and renders the notification, and activities poll + * {@link #query} for the on-screen widget. + */ +public final class DiskDownloadManager { + private static final String TAG = "DiskDownload"; + private static final int MAX_RETRIES = 10; + private static final int CONNECT_TIMEOUT_MS = 30000; + private static final int READ_TIMEOUT_MS = 30000; + private static final int BUFFER = 65536; + /** Abort + retry if throughput drops below ~0.7 KB/s over this window — i.e. + * all but stopped (catches a mirror that holds the connection open but sends + * almost nothing); genuinely slow-but-moving connections are left alone. */ + private static final long STALL_WINDOW_MS = 1500; + private static final long STALL_MIN_BYTES = 1024; + + public static final int STATE_CONNECTING = 0; + public static final int STATE_RUNNING = 1; + public static final int STATE_PAUSED = 2; // between retries + public static final int STATE_SUCCESS = 3; + public static final int STATE_FAILED = 4; + public static final int STATE_CANCELLED = 5; + + private static final AtomicLong NEXT_ID = new AtomicLong(1); + private static final ConcurrentHashMap JOBS = new ConcurrentHashMap<>(); + + private DiskDownloadManager() { + } + + /** Mutable per-download state. Fields are written by the download thread and + * read by the service / activity poll loops, hence volatile. */ + private static final class Download { + final long id; + final String url; + final String userAgent; + final String folder; + final String name; + /** Activity that started this download, reopened when its notification is tapped. */ + final String sourceActivity; + final AtomicBoolean cancelled = new AtomicBoolean(false); + volatile int state = STATE_CONNECTING; + volatile long downloaded = 0; + volatile long total = -1; + volatile String reason; + volatile UUID diskId; + /** The in-flight connection, disconnected on cancel to unblock read(). */ + volatile HttpURLConnection activeConn; + + Download(long id, String url, String userAgent, String folder, String name, + String sourceActivity) { + this.id = id; + this.url = url; + this.userAgent = userAgent; + this.folder = folder; + this.name = name; + this.sourceActivity = sourceActivity; + } + } + + /** A snapshot of a download's live state. */ + public static final class Progress { + public final int state; + public final long downloaded; + public final long total; + public final String reason; + + Progress(int state, long downloaded, long total, String reason) { + this.state = state; + this.downloaded = downloaded; + this.total = total; + this.reason = reason; + } + } + + /** What an activity needs to finish a successful import. */ + public static final class Result { + public final String folder; + public final String name; + public final UUID diskId; + + Result(String folder, String name, UUID diskId) { + this.folder = folder; + this.name = name; + this.diskId = diskId; + } + } + + /** + * Registers a download and returns its id. Drops any earlier job for the same + * destination and purges finished jobs. The caller then starts + * {@link DiskDownloadService} for this id, which runs the download. + */ + public static long enqueue( + @NonNull Context context, + @NonNull String url, + @NonNull String userAgent, + @NonNull String folder, + @NonNull String name, + @NonNull Class sourceActivity + ) { + purgeFinishedAndDuplicates(folder, name); + long id = NEXT_ID.getAndIncrement(); + JOBS.put(id, new Download(id, url, userAgent, folder, name, sourceActivity.getName())); + Log.i(TAG, fmt("Queued download #%d -> %s/%s", id, folder, name)); + return id; + } + + /** Class name of the activity that started a download, or {@code null}. */ + @Nullable + static String sourceActivity(long id) { + var d = JOBS.get(id); + return d != null ? d.sourceActivity : null; + } + + /** True if any download is still in flight (only one is allowed at a time). */ + public static boolean hasActiveDownload() { + for (var d : JOBS.values()) { + if (d.state != STATE_SUCCESS && d.state != STATE_FAILED + && d.state != STATE_CANCELLED) + return true; + } + return false; + } + + /** Live state of a download, or {@code null} if unknown/removed. */ + @Nullable + public static Progress query(long id) { + var d = JOBS.get(id); + if (d == null) return null; + return new Progress(d.state, d.downloaded, d.total, d.reason); + } + + /** Requests cancellation; the download thread aborts and deletes its partial file. */ + public static void cancel(long id) { + var d = JOBS.get(id); + if (d == null) return; + d.cancelled.set(true); + // Break any blocked read() so the cancel takes effect immediately. + var conn = d.activeConn; + if (conn != null) conn.disconnect(); + } + + /** Result of a finished download, or {@code null} if it isn't successfully done. */ + @Nullable + public static Result getResult(long id) { + var d = JOBS.get(id); + if (d == null || d.state != STATE_SUCCESS) return null; + return new Result(d.folder, d.name, d.diskId); + } + + /** Ids of all known (in-flight or just-finished) downloads. */ + @NonNull + public static long[] activeIds() { + var ids = new ArrayList<>(JOBS.keySet()); + long[] out = new long[ids.size()]; + for (int i = 0; i < out.length; i++) out[i] = ids.get(i); + return out; + } + + /** Filename of a download, or {@code null} if unknown. */ + @Nullable + public static String downloadName(long id) { + var d = JOBS.get(id); + return d != null ? d.name : null; + } + + /** + * Id of the in-flight download started by {@code sourceActivityClass}, or + * {@code -1}. Lets a re-created import screen re-attach to its running + * download and restore the progress UI. + */ + public static long activeDownloadId(@NonNull String sourceActivityClass) { + for (var d : JOBS.values()) { + if (sourceActivityClass.equals(d.sourceActivity) + && d.state != STATE_SUCCESS && d.state != STATE_FAILED + && d.state != STATE_CANCELLED) + return d.id; + } + return -1; + } + + static boolean isTerminal(long id) { + var d = JOBS.get(id); + return d == null || d.state == STATE_SUCCESS + || d.state == STATE_FAILED || d.state == STATE_CANCELLED; + } + + /** + * Streams the download to completion: resolves redirects, then retries with + * HTTP Range resume on timeouts / dropped connections. On success the file is + * moved into place and registered as a disk. Runs on a background thread. + */ + static void runDownload(@NonNull Context context, long id) { + var ctx = context.getApplicationContext(); + var d = JOBS.get(id); + if (d == null) return; + var part = new File(d.folder, d.name + ".part"); + var dest = new File(d.folder, d.name); + try { + var parent = dest.getParentFile(); + if (parent != null && !parent.exists() && !parent.mkdirs()) + throw new IOException(fmt("Cannot create folder: %s", parent.getAbsolutePath())); + // Start fresh — stale .part from a prior failed attempt may be corrupt. + if (part.exists() && !part.delete()) + Log.w(TAG, fmt("Could not clear stale part file: %s", part.getAbsolutePath())); + String lastError = null; + for (int attempt = 0; attempt <= MAX_RETRIES; attempt++) { + if (d.cancelled.get()) { + abort(d, part); + return; + } + try { + downloadOnce(d, part); + finishSuccess(ctx, d, part, dest); + return; + } catch (CancelledException e) { + abort(d, part); + return; + } catch (IOException e) { + lastError = e.getMessage() != null ? e.getMessage() : "I/O error"; + Log.w(TAG, fmt("Download #%d attempt %d failed: %s", id, attempt + 1, lastError)); + } + if (d.cancelled.get()) { + abort(d, part); + return; + } + if (attempt < MAX_RETRIES) { + d.reason = ctx.getString(R.string.download_retrying, attempt + 1, MAX_RETRIES); + d.state = STATE_PAUSED; + sleep(Math.min(2000L * (attempt + 1), 10000L)); + } + } + fail(d, part, lastError != null ? lastError : "Download failed"); + } catch (Exception e) { + Log.e(TAG, fmt("Download #%d failed", id), e); + fail(d, part, e.getMessage() != null ? e.getMessage() : "Unknown error"); + } + } + + private static void downloadOnce(Download d, File part) throws IOException { + long existing = part.exists() ? part.length() : 0; + // Re-resolve the redirect each attempt so a flaky mirror can be swapped + // out (the server load-balances), like re-running `curl -L`. + var url = resolveFinalUrl(d.url, d.userAgent); + var conn = (HttpURLConnection) new URL(url).openConnection(); + conn.setConnectTimeout(CONNECT_TIMEOUT_MS); + conn.setReadTimeout(READ_TIMEOUT_MS); + conn.setInstanceFollowRedirects(true); + conn.setRequestProperty("User-Agent", d.userAgent); + conn.setRequestProperty("Accept-Encoding", "identity"); + if (existing > 0) conn.setRequestProperty("Range", "bytes=" + existing + "-"); + d.activeConn = conn; + try { + int code = conn.getResponseCode(); + boolean resumed = code == HttpURLConnection.HTTP_PARTIAL; // 206 + if (code != HttpURLConnection.HTTP_OK && code != HttpURLConnection.HTTP_PARTIAL) + throw new IOException("HTTP " + code); + Log.i(TAG, fmt("Download #%d %s from %s (HTTP %d)", + d.id, existing > 0 ? "resuming@" + existing : "starting", conn.getURL(), code)); + // Server ignored our Range and is sending the whole file again. + long start = resumed ? existing : 0; + d.total = resolveTotal(conn, resumed, start); + d.downloaded = start; + d.state = STATE_RUNNING; + try (InputStream in = conn.getInputStream(); + OutputStream out = new FileOutputStream(part, resumed)) { + long done = start; + long windowStart = System.currentTimeMillis(); + long windowBytes = done; + var buf = new byte[BUFFER]; + int n; + while ((n = in.read(buf)) != -1) { + if (d.cancelled.get()) throw new CancelledException(); + out.write(buf, 0, n); + done += n; + d.downloaded = done; + long now = System.currentTimeMillis(); + if (now - windowStart >= STALL_WINDOW_MS) { + if (done - windowBytes < STALL_MIN_BYTES) + throw new IOException("Throughput too low"); + windowStart = now; + windowBytes = done; + } + } + out.flush(); + // EOF before the expected size means the connection dropped early. + if (d.total > 0 && done < d.total) + throw new IOException(fmt("Incomplete: %d/%d bytes", done, d.total)); + } + } finally { + d.activeConn = null; + conn.disconnect(); + } + } + + /** + * Total file size for the progress bar. On a 206 resume the body may be + * chunked (no Content-Length), so prefer the absolute total from the + * {@code Content-Range: bytes a-b/total} header; otherwise derive it from + * Content-Length. Returns -1 if genuinely unknown. + */ + private static long resolveTotal(HttpURLConnection conn, boolean resumed, long start) { + if (resumed) { + var range = conn.getHeaderField("Content-Range"); // "bytes a-b/total" + if (range != null) { + int slash = range.lastIndexOf('/'); + if (slash >= 0) { + var total = range.substring(slash + 1).trim(); + if (!total.equals("*")) { + try { + return Long.parseLong(total); + } catch (NumberFormatException ignored) { + } + } + } + } + long len = conn.getContentLengthLong(); + return len >= 0 ? start + len : -1; + } + long len = conn.getContentLengthLong(); + return len >= 0 ? len : -1; + } + + private static void finishSuccess(Context ctx, Download d, File part, File dest) + throws IOException { + if (d.cancelled.get()) { + abort(d, part); + return; + } + if (dest.exists() && !dest.delete()) + throw new IOException(fmt("Cannot overwrite: %s", dest.getAbsolutePath())); + if (!part.renameTo(dest)) { + copyFile(part, dest); + if (!part.delete()) Log.w(TAG, fmt("Failed to delete part: %s", part.getAbsolutePath())); + } + var config = registerDisk(ctx, d.name, d.folder); + d.diskId = config.getId(); + d.reason = null; + d.state = STATE_SUCCESS; + Log.i(TAG, fmt("Download #%d complete -> %s", d.id, dest.getAbsolutePath())); + } + + private static void abort(Download d, File part) { + if (part.exists() && !part.delete()) + Log.w(TAG, fmt("Failed to delete part: %s", part.getAbsolutePath())); + d.state = STATE_CANCELLED; + } + + private static void fail(Download d, File part, String reason) { + if (part.exists() && !part.delete()) + Log.w(TAG, fmt("Failed to delete part: %s", part.getAbsolutePath())); + d.reason = reason; + d.state = STATE_FAILED; + } + + /** + * Follows redirects (including cross-protocol http↔https) and returns the + * final URL. Returns the original URL if resolution fails. + */ + @NonNull + static String resolveFinalUrl(@NonNull String url, @NonNull String userAgent) { + var current = url; + try { + for (int i = 0; i < 6; i++) { + var conn = (HttpURLConnection) new URL(current).openConnection(); + conn.setInstanceFollowRedirects(false); + conn.setConnectTimeout(CONNECT_TIMEOUT_MS); + conn.setReadTimeout(CONNECT_TIMEOUT_MS); + conn.setRequestProperty("User-Agent", userAgent); + conn.setRequestProperty("Accept-Encoding", "identity"); + int code = conn.getResponseCode(); + String location = (code >= 300 && code < 400) + ? conn.getHeaderField("Location") : null; + conn.disconnect(); + if (location == null || location.isEmpty()) break; + current = new URL(new URL(current), location).toString(); + } + } catch (Exception e) { + Log.w(TAG, fmt("Redirect resolution failed for %s; using it as-is", url), e); + return url; + } + if (!current.equals(url)) Log.i(TAG, fmt("Resolved %s -> %s", url, current)); + return current; + } + + private static DiskConfig registerDisk(Context ctx, String name, String folder) { + var store = new DiskStore(); + store.load(ctx); + var config = new DiskConfig(); + config.setName(name); + config.item.set("folder", folder); + store.add(config); + store.save(ctx); + return config; + } + + private static void copyFile(File src, File dest) throws IOException { + try (InputStream in = new FileInputStream(src); + OutputStream out = new FileOutputStream(dest)) { + var buf = new byte[BUFFER]; + int n; + while ((n = in.read(buf)) != -1) out.write(buf, 0, n); + out.flush(); + } + } + + /** Drops finished jobs and any earlier job aimed at the same destination. */ + private static void purgeFinishedAndDuplicates(String folder, String name) { + for (var e : JOBS.entrySet()) { + var d = e.getValue(); + boolean duplicate = d.folder.equals(folder) && d.name.equals(name); + boolean finished = d.state == STATE_SUCCESS + || d.state == STATE_FAILED || d.state == STATE_CANCELLED; + if (duplicate) d.cancelled.set(true); + if (duplicate || finished) JOBS.remove(e.getKey()); + } + } + + private static void sleep(long ms) { + try { + Thread.sleep(ms); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + /** Thrown to unwind the read loop on cancellation (not retried). */ + private static final class CancelledException extends IOException { + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/lib/download/DiskDownloadService.java b/app/src/main/java/cn/classfun/droidvm/lib/download/DiskDownloadService.java new file mode 100644 index 000000000..90bedd573 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/lib/download/DiskDownloadService.java @@ -0,0 +1,326 @@ +package cn.classfun.droidvm.lib.download; + +import static cn.classfun.droidvm.lib.size.SizeUtils.formatSize; +import static cn.classfun.droidvm.lib.utils.StringUtils.formatDuration; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ServiceInfo; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.app.NotificationCompat; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import cn.classfun.droidvm.R; +import cn.classfun.droidvm.lib.download.DiskDownloadManager.Progress; +import cn.classfun.droidvm.ui.main.MainActivity; + +/** + * Foreground service that runs {@link DiskDownloadManager#runDownload} on a + * background thread and renders one notification per download covering every + * state — connecting, downloading (%/size/speed/ETA), retrying, failed, done — + * in the same notification (no separate completion/failure notification). + */ +public final class DiskDownloadService extends Service { + private static final String TAG = "DiskDownloadService"; + private static final String ACTION_START = "cn.classfun.droidvm.download.START"; + private static final String ACTION_CANCEL = "cn.classfun.droidvm.download.CANCEL"; + private static final String EXTRA_ID = "download_id"; + private static final String CHANNEL_ID = "disk_downloads_progress"; + private static final int NOTIF_BASE = 0x44_44_00_00; + private static final long POLL_MS = 1000; + private static final long SPEED_WINDOW_MS = 800; + + private final Handler handler = new Handler(Looper.getMainLooper()); + private final ExecutorService executor = Executors.newCachedThreadPool(); + /** Ids with a live (ongoing) notification. */ + private final Set active = new LinkedHashSet<>(); + /** Ids already handed to the executor, so a repeat START doesn't double-run. */ + private final Set submitted = new HashSet<>(); + private final java.util.Map trackers = new java.util.HashMap<>(); + private NotificationManager nm; + private long foregroundId = -1; + private boolean polling = false; + + /** Per-download moving-average speed estimate. */ + private static final class Tracker { + long lastTime = 0; + long lastBytes = 0; + long speed = 0; + } + + public static void start(@NonNull Context ctx, long id) { + var i = new Intent(ctx, DiskDownloadService.class) + .setAction(ACTION_START) + .putExtra(EXTRA_ID, id); + ctx.startForegroundService(i); + } + + @Override + public void onCreate() { + super.onCreate(); + nm = getSystemService(NotificationManager.class); + ensureChannel(); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (intent != null) { + long id = intent.getLongExtra(EXTRA_ID, -1); + var action = intent.getAction(); + if (ACTION_CANCEL.equals(action) && id >= 0) { + DiskDownloadManager.cancel(id); + } else if (ACTION_START.equals(action) && id >= 0) { + active.add(id); + if (submitted.add(id)) { + var appCtx = getApplicationContext(); + executor.submit(() -> DiskDownloadManager.runDownload(appCtx, id)); + } + } + } + if (active.isEmpty()) { + stopForegroundAndSelf(); + return START_NOT_STICKY; + } + promoteForeground(); + startPolling(); + return START_NOT_STICKY; + } + + @Override + public void onTimeout(int startId, int fgsType) { + Log.w(TAG, "Foreground service timed out; stopping"); + stopForegroundAndSelf(); + } + + @Nullable + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public void onDestroy() { + super.onDestroy(); + handler.removeCallbacks(pollTask); + executor.shutdownNow(); + } + + private void startPolling() { + if (polling) return; + polling = true; + handler.post(pollTask); + } + + private final Runnable pollTask = new Runnable() { + @Override + public void run() { + tick(); + if (active.isEmpty()) { + polling = false; + stopForegroundAndSelf(); + } else { + handler.postDelayed(this, POLL_MS); + } + } + }; + + private void tick() { + for (long id : new ArrayList<>(active)) { + var p = DiskDownloadManager.query(id); + if (p == null) { + active.remove(id); + if (nm != null) nm.cancel(notifId(id)); + continue; + } + switch (p.state) { + case DiskDownloadManager.STATE_SUCCESS: + if (nm != null) nm.notify(notifId(id), buildDone(id)); + active.remove(id); + break; + case DiskDownloadManager.STATE_FAILED: + if (nm != null) nm.notify(notifId(id), buildFailed(id, p)); + active.remove(id); + break; + case DiskDownloadManager.STATE_CANCELLED: + if (nm != null) nm.cancel(notifId(id)); + active.remove(id); + break; + default: + if (nm != null) nm.notify(notifId(id), buildOngoing(id, p)); + break; + } + } + if (!active.isEmpty() && !active.contains(foregroundId)) + promoteForeground(); + } + + /** Anchors the foreground on a still-running download (its notification must + * be ongoing). Stops the service if nothing is running. */ + private void promoteForeground() { + if (active.contains(foregroundId)) return; + for (long id : new ArrayList<>(active)) { + var p = DiskDownloadManager.query(id); + if (p == null) { + active.remove(id); + continue; + } + foregroundId = id; + startForeground(notifId(id), buildOngoing(id, p), + ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC); + return; + } + stopForegroundAndSelf(); + } + + private void stopForegroundAndSelf() { + handler.removeCallbacks(pollTask); + polling = false; + foregroundId = -1; + // DETACH so finished (success/failed) notifications stay in the shade. + stopForeground(STOP_FOREGROUND_DETACH); + stopSelf(); + } + + private Notification buildOngoing(long id, @NonNull Progress p) { + var b = baseBuilder(id) + .setOngoing(true) + .addAction(0, getString(R.string.notif_download_cancel), cancelIntent(this, id)); + if (p.state == DiskDownloadManager.STATE_PAUSED) { + applyBar(b, p); + b.setContentText(p.reason != null ? p.reason : getString(R.string.download_paused)); + } else if (p.state == DiskDownloadManager.STATE_RUNNING) { + applyBar(b, p); + b.setContentText(runningText(p, updateSpeed(id, p.downloaded))); + } else { // connecting / no size yet + b.setProgress(0, 0, true); + b.setContentText(getString(R.string.notif_download_connecting)); + } + return b.build(); + } + + private Notification buildDone(long id) { + return baseBuilder(id) + .setContentText(getString(R.string.notif_download_complete_title)) + .setAutoCancel(true) + .build(); + } + + private Notification buildFailed(long id, @NonNull Progress p) { + var reason = p.reason != null ? p.reason : "Unknown error"; + return baseBuilder(id) + .setContentText(getString(R.string.download_failed, reason)) + .setStyle(new NotificationCompat.BigTextStyle() + .bigText(getString(R.string.download_failed, reason))) + .setAutoCancel(true) + .build(); + } + + private NotificationCompat.Builder baseBuilder(long id) { + var name = DiskDownloadManager.downloadName(id); + return new NotificationCompat.Builder(this, CHANNEL_ID) + .setSmallIcon(R.drawable.ic_download) + .setContentTitle(name != null ? name : getString(R.string.app_name)) + .setOnlyAlertOnce(true) + .setContentIntent(contentIntent(id)); + } + + private void applyBar(NotificationCompat.Builder b, Progress p) { + if (p.total > 0) { + b.setProgress(1000, (int) (p.downloaded * 1000 / p.total), false); + b.setSubText(getString(R.string.download_percent, p.downloaded * 100f / p.total)); + } else { + b.setProgress(0, 0, true); + } + } + + private String runningText(Progress p, long speed) { + var text = p.total > 0 + ? getString(R.string.download_size_progress, formatSize(p.downloaded), formatSize(p.total)) + : getString(R.string.download_size_unknown, formatSize(p.downloaded)); + if (speed > 0) { + text += " · " + getString(R.string.download_speed, formatSize(speed)); + if (p.total > 0) + text += " · " + getString(R.string.download_eta, + formatDuration((p.total - p.downloaded) / speed)); + } + return text; + } + + private long updateSpeed(long id, long downloaded) { + var t = trackers.computeIfAbsent(id, k -> new Tracker()); + long now = System.currentTimeMillis(); + if (t.lastTime == 0) { + t.lastTime = now; + t.lastBytes = downloaded; + return t.speed; + } + long elapsed = now - t.lastTime; + if (elapsed >= SPEED_WINDOW_MS) { + t.speed = elapsed > 0 ? (downloaded - t.lastBytes) * 1000 / elapsed : 0; + t.lastTime = now; + t.lastBytes = downloaded; + } + return t.speed; + } + + private PendingIntent contentIntent(long id) { + // Reopen the import screen that started this download (bring its existing + // instance to the front; create it if it's gone). Fall back to launching + // the app if we don't know the source. + var source = DiskDownloadManager.sourceActivity(id); + Intent intent; + if (source != null) { + intent = new Intent().setClassName(this, source) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_CLEAR_TOP + | Intent.FLAG_ACTIVITY_SINGLE_TOP); + } else { + intent = getPackageManager().getLaunchIntentForPackage(getPackageName()); + if (intent == null) intent = new Intent(this, MainActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + } + return PendingIntent.getActivity(this, (int) id, intent, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); + } + + private static PendingIntent cancelIntent(Context ctx, long id) { + var intent = new Intent(ctx, DiskDownloadService.class) + .setAction(ACTION_CANCEL) + .putExtra(EXTRA_ID, id); + return PendingIntent.getService(ctx, (int) id, intent, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); + } + + private void ensureChannel() { + if (nm == null) return; + var ch = new NotificationChannel( + CHANNEL_ID, + getString(R.string.notif_channel_downloads_progress), + NotificationManager.IMPORTANCE_LOW + ); + ch.setDescription(getString(R.string.notif_channel_downloads_progress_desc)); + ch.setShowBadge(false); + nm.createNotificationChannel(ch); + } + + private static int notifId(long id) { + return NOTIF_BASE + (int) (id & 0xFFFF); + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/ui/disk/download/ImportURLActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/disk/download/ImportURLActivity.java index f313b1dca..b996c353d 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/disk/download/ImportURLActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/disk/download/ImportURLActivity.java @@ -5,6 +5,7 @@ import static android.widget.Toast.LENGTH_SHORT; import static com.google.android.material.R.attr.colorOnSurfaceVariant; import static cn.classfun.droidvm.lib.utils.FileUtils.externalPath; +import static cn.classfun.droidvm.lib.utils.NetUtils.USER_AGENT; import static cn.classfun.droidvm.lib.utils.NetUtils.extractFileName; import static cn.classfun.droidvm.lib.utils.StringUtils.pathJoin; import static cn.classfun.droidvm.lib.utils.StringUtils.resolveUriPath; @@ -15,6 +16,8 @@ import android.content.Intent; import android.net.Uri; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; import android.util.Log; import android.view.View; import android.widget.TextView; @@ -23,6 +26,7 @@ import androidx.activity.OnBackPressedCallback; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import androidx.core.widget.NestedScrollView; @@ -31,7 +35,6 @@ import com.google.android.material.appbar.MaterialToolbar; import com.google.android.material.button.MaterialButton; import com.google.android.material.card.MaterialCardView; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton; import com.google.android.material.progressindicator.CircularProgressIndicator; @@ -42,18 +45,18 @@ import java.util.concurrent.Executors; import cn.classfun.droidvm.R; +import cn.classfun.droidvm.lib.download.DiskDownloadManager; +import cn.classfun.droidvm.lib.download.DiskDownloadService; import cn.classfun.droidvm.lib.ui.SimpleTextWatcher; import cn.classfun.droidvm.lib.utils.NetUtils.HttpException; -import cn.classfun.droidvm.lib.store.disk.DiskConfig; -import cn.classfun.droidvm.lib.store.disk.DiskStore; import cn.classfun.droidvm.ui.disk.create.DiskFormat; import cn.classfun.droidvm.ui.widgets.tools.DownloadWidget; -import cn.classfun.droidvm.ui.widgets.tools.DownloadWidget.OnDownloadListener; import cn.classfun.droidvm.ui.widgets.tools.KernelAnalysisWidget; import cn.classfun.droidvm.ui.widgets.row.TextInputRowWidget; -public final class ImportURLActivity extends AppCompatActivity implements OnDownloadListener { +public final class ImportURLActivity extends AppCompatActivity { private static final String TAG = "ImportURL"; + private static final long POLL_INTERVAL_MS = 500; private final ExecutorService executor = Executors.newSingleThreadExecutor(); private TextInputRowWidget inputUrl, inputFilename, inputFolder; private TextView tvStatus; @@ -72,6 +75,8 @@ public final class ImportURLActivity extends AppCompatActivity implements OnDown private boolean isDownloading = false; private String downloadName = null; private String downloadFolder = null; + private long currentDownloadId = -1; + private final Handler pollHandler = new Handler(Looper.getMainLooper()); private ActivityResultLauncher folderPickerLauncher; @Override @@ -127,27 +132,78 @@ public void onTextChanged(CharSequence s, int st, int b, int c) { btnLoad.setOnClickListener(v -> doLoad()); fabImport.setOnClickListener(v -> doImport()); setStatusIdle(); + // Restore the whole form if re-created mid-download; else just the progress bar. + if (!restoreSession()) reattachActiveDownload(); + } + + /** Snapshot of the form, kept across activity re-creation while a download runs. */ + private static final class Session { + String url, filename, folder; + boolean infoLoaded; + String infoSize, infoType; + } + + /** The in-progress import (only one download runs at a time), or {@code null}. */ + private static Session session; + + private void captureSession() { + var s = new Session(); + s.url = inputUrl.getText(); + s.filename = inputFilename.getText(); + s.folder = inputFolder.getText(); + s.infoLoaded = infoLoaded; + s.infoSize = tvInfoSize.getText().toString(); + s.infoType = tvInfoType.getText().toString(); + session = s; + } + + private boolean restoreSession() { + var s = session; + if (s == null) return false; + inputUrl.setText(s.url); + inputFilename.setText(s.filename); + inputFolder.setText(s.folder); + if (s.infoLoaded) { + infoLoaded = true; + tvInfoSize.setText(s.infoSize); + tvInfoType.setText(s.infoType); + cardInfo.setVisibility(VISIBLE); + } + reattachActiveDownload(); + return true; + } + + /** Re-attaches just the progress bar to the running download (no form state). */ + private void reattachActiveDownload() { + long id = DiskDownloadManager.activeDownloadId(getClass().getName()); + if (id < 0) return; + currentDownloadId = id; + isDownloading = true; + setInputsEnabled(false); + fabImport.setVisibility(GONE); + downloadWidget.setVisibility(VISIBLE); + var name = DiskDownloadManager.downloadName(id); + downloadWidget.startExternal(name != null ? name : "", this::cancelDownload); + scrollView.post(() -> scrollView.fullScroll(View.FOCUS_DOWN)); + pollHandler.post(pollRunnable); } @Override protected void onDestroy() { super.onDestroy(); - if (downloadWidget != null) - downloadWidget.stopAndCleanup(); + // Stop driving the widget; the download keeps running in the foreground + // service (notification shade) and registers the disk itself. + pollHandler.removeCallbacks(pollRunnable); executor.shutdownNow(); } private void confirmExit() { - if (isDownloading && downloadWidget != null && downloadWidget.isRunning()) { - new MaterialAlertDialogBuilder(this) - .setTitle(R.string.download_exit_title) - .setMessage(R.string.download_exit_message) - .setNegativeButton(android.R.string.cancel, null) - .setPositiveButton(android.R.string.ok, (d, w) -> finish()) - .show(); - } else { - finish(); - } + // Leave normally (so you can navigate to other screens while downloading). + // The download keeps running in the foreground service; the screen's state + // is preserved and restored when re-opened. + if (isDownloading && currentDownloadId >= 0) + Toast.makeText(this, R.string.download_background_toast, LENGTH_SHORT).show(); + finish(); } private void onFolderPickerResult(Uri uri) { @@ -252,6 +308,10 @@ private void onHeadLoaded(long contentLength, String contentType, String fileNam private void doImport() { if (isDownloading) return; + if (DiskDownloadManager.hasActiveDownload()) { + Toast.makeText(this, R.string.download_one_at_a_time, LENGTH_SHORT).show(); + return; + } var urlStr = inputUrl.getText(); if (urlStr.isEmpty()) { inputUrl.setError(getString(R.string.import_url_error_url_empty)); @@ -285,59 +345,120 @@ private void doImport() { } downloadName = name; downloadFolder = folder; - startDownload(urlStr, destPath); + startDownload(urlStr); } - private void startDownload(String url, String destPath) { + private void startDownload(String url) { isDownloading = true; + captureSession(); setInputsEnabled(false); fabImport.setVisibility(GONE); downloadWidget.setVisibility(VISIBLE); scrollView.post(() -> scrollView.fullScroll(View.FOCUS_DOWN)); - downloadWidget.setOnDownloadListener(this); - downloadWidget.start(url, destPath); + downloadWidget.startExternal(downloadName, this::cancelDownload); + final var folder = downloadFolder; + final var name = downloadName; + // enqueue() resolves redirects (network I/O), so run it off the main thread. + runOnPool(() -> { + long id = DiskDownloadManager.enqueue( + this, url, USER_AGENT, folder, name, ImportURLActivity.class); + runOnUiThread(() -> onDownloadEnqueued(id)); + }); } - @Override - public void onFinished(DownloadWidget widget, File file) { - var config = new DiskConfig(); - config.setName(downloadName); - config.item.set("folder", downloadFolder); - Runnable done = () -> { - var resultData = new Intent(); - resultData.putExtra("result_disk_path", config.getFullPath()); - setResult(RESULT_OK, resultData); - if (config.getFormat() == DiskFormat.QCOW2) - startOptimize(this, config.getId()); + private void onDownloadEnqueued(long id) { + if (id < 0) { + if (isDownloading) onDownloadFailed(getString(R.string.download_error_start)); + return; + } + if (!isDownloading) { + // Cancelled while still enqueueing. + DiskDownloadManager.cancel(id); + return; + } + currentDownloadId = id; + DiskDownloadService.start(this, id); + if (!isDestroyed()) pollHandler.post(pollRunnable); + } + + /** Mirrors the download's live state into the on-screen widget. */ + private final Runnable pollRunnable = new Runnable() { + @Override + public void run() { + if (currentDownloadId < 0) return; + var p = DiskDownloadManager.query(currentDownloadId); + if (p == null) { + cancelDownload(); // job gone (cancelled elsewhere) + return; + } + switch (p.state) { + case DiskDownloadManager.STATE_SUCCESS: + onDownloadSucceeded(); + break; + case DiskDownloadManager.STATE_FAILED: + onDownloadFailed(p.reason); + break; + case DiskDownloadManager.STATE_CANCELLED: + cancelDownload(); + break; + case DiskDownloadManager.STATE_PAUSED: + downloadWidget.updateExternal(p.downloaded, p.total); + downloadWidget.markExternalPaused( + p.reason != null ? p.reason : getString(R.string.download_paused)); + pollHandler.postDelayed(this, POLL_INTERVAL_MS); + break; + default: // CONNECTING, RUNNING + downloadWidget.updateExternal(p.downloaded, p.total); + pollHandler.postDelayed(this, POLL_INTERVAL_MS); + break; + } + } + }; + + private void onDownloadSucceeded() { + long id = currentDownloadId; + currentDownloadId = -1; + pollHandler.removeCallbacks(pollRunnable); + downloadWidget.markExternalFinished(); + var result = DiskDownloadManager.getResult(id); + if (result == null) { finish(); - }; - runOnPool(() -> { - var store = new DiskStore(); - store.load(this); - store.add(config); - store.save(this); - runOnUiThread(done); - }); + return; + } Toast.makeText( this, - getString(R.string.import_url_success, downloadName), + getString(R.string.import_url_success, result.name), LENGTH_SHORT ).show(); + var resultData = new Intent(); + resultData.putExtra("result_disk_path", pathJoin(result.folder, result.name)); + setResult(RESULT_OK, resultData); + if (result.diskId != null && DiskFormat.fromFilename(result.name) == DiskFormat.QCOW2) + startOptimize(this, result.diskId); + finish(); } - @Override - public void onFailed(DownloadWidget widget, String error) { + private void cancelDownload() { + long id = currentDownloadId; + currentDownloadId = -1; + pollHandler.removeCallbacks(pollRunnable); + if (id >= 0) DiskDownloadManager.cancel(id); + downloadWidget.markExternalCancelled(); isDownloading = false; setInputsEnabled(true); fabImport.setVisibility(VISIBLE); + downloadWidget.setVisibility(GONE); } - @Override - public void onCancelled(DownloadWidget widget) { + private void onDownloadFailed(@Nullable String reason) { + long id = currentDownloadId; + currentDownloadId = -1; + pollHandler.removeCallbacks(pollRunnable); + if (id >= 0) DiskDownloadManager.cancel(id); + downloadWidget.markExternalFailed(reason); isDownloading = false; setInputsEnabled(true); fabImport.setVisibility(VISIBLE); - downloadWidget.setVisibility(GONE); } private void setInputsEnabled(boolean enabled) { diff --git a/app/src/main/java/cn/classfun/droidvm/ui/disk/images/ImportImagesActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/disk/images/ImportImagesActivity.java index cfd3bd018..c988ee9e8 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/disk/images/ImportImagesActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/disk/images/ImportImagesActivity.java @@ -14,6 +14,8 @@ import android.content.Intent; import android.net.Uri; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; import android.view.View; import android.widget.TextView; import android.widget.Toast; @@ -29,7 +31,6 @@ import com.google.android.material.appbar.CollapsingToolbarLayout; import com.google.android.material.appbar.MaterialToolbar; import com.google.android.material.card.MaterialCardView; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton; import java.io.File; @@ -37,16 +38,16 @@ import cn.classfun.droidvm.R; import cn.classfun.droidvm.lib.data.Repos; -import cn.classfun.droidvm.lib.store.disk.DiskConfig; -import cn.classfun.droidvm.lib.store.disk.DiskStore; +import cn.classfun.droidvm.lib.download.DiskDownloadManager; +import cn.classfun.droidvm.lib.download.DiskDownloadService; import cn.classfun.droidvm.lib.ui.IconItemAdapter; import cn.classfun.droidvm.ui.disk.create.DiskFormat; import cn.classfun.droidvm.ui.widgets.row.DropdownRowWidget; import cn.classfun.droidvm.ui.widgets.row.TextInputRowWidget; import cn.classfun.droidvm.ui.widgets.tools.DownloadWidget; -import cn.classfun.droidvm.ui.widgets.tools.DownloadWidget.OnDownloadListener; -public final class ImportImagesActivity extends AppCompatActivity implements OnDownloadListener { +public final class ImportImagesActivity extends AppCompatActivity { + private static final long POLL_INTERVAL_MS = 500; private Repos repos; private CollapsingToolbarLayout collapsingToolbar; private MaterialToolbar toolbar; @@ -68,6 +69,8 @@ public final class ImportImagesActivity extends AppCompatActivity implements OnD private boolean isDownloading = false; private String downloadName = null; private String downloadFolder = null; + private long currentDownloadId = -1; + private final Handler pollHandler = new Handler(Looper.getMainLooper()); private FlatImage selectedImage; private ActivityResultLauncher folderPickerLauncher; @@ -111,13 +114,60 @@ public void handleOnBackPressed() { cardPlaceholder.setOnClickListener(v -> showImagePicker()); cardSelected.setOnClickListener(v -> showImagePicker()); hideSelection(); + // Restore the whole form if re-created mid-download; else just the progress bar. + if (!restoreSession()) reattachActiveDownload(); + } + + /** Snapshot of the form, kept across activity re-creation while a download runs. */ + private static final class Session { + FlatImage image; + String mirror, filename, folder; + } + + /** The in-progress import (only one download runs at a time), or {@code null}. */ + private static Session session; + + private void captureSession() { + var s = new Session(); + s.image = selectedImage; + s.mirror = dropdownMirror.getText(); + s.filename = inputFilename.getText(); + s.folder = inputFolder.getText(); + session = s; + } + + private boolean restoreSession() { + var s = session; + if (s == null || s.image == null) return false; + onImageSelected(s.image); + dropdownMirror.setText(s.mirror); + updateInfoCard(); + inputFilename.setText(s.filename); + inputFolder.setText(s.folder); + reattachActiveDownload(); + return true; + } + + /** Re-attaches just the progress bar to the running download (no form state). */ + private void reattachActiveDownload() { + long id = DiskDownloadManager.activeDownloadId(getClass().getName()); + if (id < 0) return; + currentDownloadId = id; + isDownloading = true; + setInputsEnabled(false); + fabDownload.setVisibility(GONE); + downloadWidget.setVisibility(VISIBLE); + var name = DiskDownloadManager.downloadName(id); + downloadWidget.startExternal(name != null ? name : "", this::cancelDownload); + scrollView.post(() -> scrollView.fullScroll(View.FOCUS_DOWN)); + pollHandler.post(pollRunnable); } @Override protected void onDestroy() { super.onDestroy(); - if (downloadWidget != null) - downloadWidget.stopAndCleanup(); + // The download keeps running in the foreground service; just stop polling. + pollHandler.removeCallbacks(pollRunnable); } private void onChooseFolder() { @@ -131,16 +181,12 @@ private void onFolderPickerResult(Uri uri) { } private void confirmExit() { - if (isDownloading && downloadWidget != null && downloadWidget.isRunning()) { - new MaterialAlertDialogBuilder(this) - .setTitle(R.string.download_exit_title) - .setMessage(R.string.download_exit_message) - .setNegativeButton(android.R.string.cancel, null) - .setPositiveButton(android.R.string.ok, (d, w) -> finish()) - .show(); - } else { - finish(); - } + // Leave normally (so you can navigate to other screens while downloading). + // The download keeps running in the foreground service; the screen's state + // is preserved and restored when re-opened. + if (isDownloading && currentDownloadId >= 0) + Toast.makeText(this, R.string.download_background_toast, LENGTH_SHORT).show(); + finish(); } private void showImagePicker() { @@ -230,6 +276,10 @@ private String getSelectedMirrorKey() { private void doDownload() { if (isDownloading) return; + if (DiskDownloadManager.hasActiveDownload()) { + Toast.makeText(this, R.string.download_one_at_a_time, LENGTH_SHORT).show(); + return; + } if (selectedImage == null) { Toast.makeText(this, R.string.images_error_no_image, LENGTH_SHORT).show(); return; @@ -254,55 +304,116 @@ private void doDownload() { downloadName = name; downloadFolder = folder; var url = resolveDownloadUrl(); - startDownload(url, destPath); + startDownload(url); } - private void startDownload(String url, String destPath) { + private void startDownload(String url) { isDownloading = true; + captureSession(); setInputsEnabled(false); fabDownload.setVisibility(GONE); downloadWidget.setVisibility(VISIBLE); scrollView.post(() -> scrollView.fullScroll(View.FOCUS_DOWN)); - downloadWidget.setOnDownloadListener(this); - downloadWidget.setUserAgent(DL_USER_AGENT); - downloadWidget.start(url, destPath); + downloadWidget.startExternal(downloadName, this::cancelDownload); + final var folder = downloadFolder; + final var name = downloadName; + // enqueue() resolves redirects (network I/O), so run it off the main thread. + runOnPool(() -> { + long id = DiskDownloadManager.enqueue( + this, url, DL_USER_AGENT, folder, name, ImportImagesActivity.class); + runOnUiThread(() -> onDownloadEnqueued(id)); + }); } - @Override - public void onFinished(DownloadWidget widget, File file) { - var config = new DiskConfig(); - config.setName(downloadName); - config.item.set("folder", downloadFolder); - Runnable done = () -> { - var resultData = new Intent(); - resultData.putExtra("result_disk_path", config.getFullPath()); - setResult(RESULT_OK, resultData); - if (config.getFormat() == DiskFormat.QCOW2) - startOptimize(this, config.getId()); + private void onDownloadEnqueued(long id) { + if (id < 0) { + if (isDownloading) onDownloadFailed(getString(R.string.download_error_start)); + return; + } + if (!isDownloading) { + // Cancelled while still enqueueing. + DiskDownloadManager.cancel(id); + return; + } + currentDownloadId = id; + DiskDownloadService.start(this, id); + if (!isDestroyed()) pollHandler.post(pollRunnable); + } + + /** Mirrors the download's live state into the on-screen widget. */ + private final Runnable pollRunnable = new Runnable() { + @Override + public void run() { + if (currentDownloadId < 0) return; + var p = DiskDownloadManager.query(currentDownloadId); + if (p == null) { + cancelDownload(); // job gone (cancelled elsewhere) + return; + } + switch (p.state) { + case DiskDownloadManager.STATE_SUCCESS: + onDownloadSucceeded(); + break; + case DiskDownloadManager.STATE_FAILED: + onDownloadFailed(p.reason); + break; + case DiskDownloadManager.STATE_CANCELLED: + cancelDownload(); + break; + case DiskDownloadManager.STATE_PAUSED: + downloadWidget.updateExternal(p.downloaded, p.total); + downloadWidget.markExternalPaused( + p.reason != null ? p.reason : getString(R.string.download_paused)); + pollHandler.postDelayed(this, POLL_INTERVAL_MS); + break; + default: // CONNECTING, RUNNING + downloadWidget.updateExternal(p.downloaded, p.total); + pollHandler.postDelayed(this, POLL_INTERVAL_MS); + break; + } + } + }; + + private void onDownloadSucceeded() { + long id = currentDownloadId; + currentDownloadId = -1; + pollHandler.removeCallbacks(pollRunnable); + downloadWidget.markExternalFinished(); + var result = DiskDownloadManager.getResult(id); + if (result == null) { finish(); - }; - runOnPool(() -> { - var store = new DiskStore(); - store.load(this); - store.add(config); - store.save(this); - runOnUiThread(done); - }); + return; + } Toast.makeText(this, - getString(R.string.images_success, downloadName), + getString(R.string.images_success, result.name), LENGTH_SHORT ).show(); + var resultData = new Intent(); + resultData.putExtra("result_disk_path", pathJoin(result.folder, result.name)); + setResult(RESULT_OK, resultData); + if (result.diskId != null && DiskFormat.fromFilename(result.name) == DiskFormat.QCOW2) + startOptimize(this, result.diskId); + finish(); } - @Override - public void onFailed(DownloadWidget widget, String error) { + private void cancelDownload() { + long id = currentDownloadId; + currentDownloadId = -1; + pollHandler.removeCallbacks(pollRunnable); + if (id >= 0) DiskDownloadManager.cancel(id); + downloadWidget.markExternalCancelled(); isDownloading = false; setInputsEnabled(true); fabDownload.setVisibility(VISIBLE); + downloadWidget.setVisibility(GONE); } - @Override - public void onCancelled(DownloadWidget widget) { + private void onDownloadFailed(@Nullable String reason) { + long id = currentDownloadId; + currentDownloadId = -1; + pollHandler.removeCallbacks(pollRunnable); + if (id >= 0) DiskDownloadManager.cancel(id); + downloadWidget.markExternalFailed(reason); isDownloading = false; setInputsEnabled(true); fabDownload.setVisibility(VISIBLE); diff --git a/app/src/main/java/cn/classfun/droidvm/ui/disk/lxc/ImportLxcImagesActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/disk/lxc/ImportLxcImagesActivity.java index b6cec1d6c..5779c7a09 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/disk/lxc/ImportLxcImagesActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/disk/lxc/ImportLxcImagesActivity.java @@ -20,6 +20,8 @@ import android.content.Intent; import android.net.Uri; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; import android.util.Log; import android.view.View; import android.widget.TextView; @@ -37,7 +39,6 @@ import com.google.android.material.appbar.MaterialToolbar; import com.google.android.material.button.MaterialButton; import com.google.android.material.card.MaterialCardView; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton; import com.google.android.material.progressindicator.CircularProgressIndicator; @@ -54,19 +55,19 @@ import cn.classfun.droidvm.lib.api.ApiManager; import cn.classfun.droidvm.lib.api.Privacy; import cn.classfun.droidvm.lib.data.Repos; -import cn.classfun.droidvm.lib.store.disk.DiskConfig; -import cn.classfun.droidvm.lib.store.disk.DiskStore; +import cn.classfun.droidvm.lib.download.DiskDownloadManager; +import cn.classfun.droidvm.lib.download.DiskDownloadService; import cn.classfun.droidvm.lib.ui.IconItemAdapter; import cn.classfun.droidvm.ui.disk.create.DiskFormat; import cn.classfun.droidvm.ui.widgets.row.DropdownRowWidget; import cn.classfun.droidvm.ui.widgets.row.TextInputRowWidget; import cn.classfun.droidvm.ui.widgets.tools.DownloadWidget; -import cn.classfun.droidvm.ui.widgets.tools.DownloadWidget.OnDownloadListener; import cn.classfun.droidvm.ui.widgets.tools.KernelAnalysisWidget; -public final class ImportLxcImagesActivity extends AppCompatActivity implements OnDownloadListener { +public final class ImportLxcImagesActivity extends AppCompatActivity { private static final String TAG = "ImportLxcImages"; private static final String IMAGES_META_PATH = "/streams/v1/images.json"; + private static final long POLL_INTERVAL_MS = 500; private final Map displayVersionToRelease = new LinkedHashMap<>(); private Repos.Repo lxcRepo; private String[] metaSourceKeys; @@ -101,6 +102,8 @@ public final class ImportLxcImagesActivity extends AppCompatActivity implements private String downloadFolder = null; private ApiManager apiManager = null; private ActivityResultLauncher folderPickerLauncher; + private long currentDownloadId = -1; + private final Handler pollHandler = new Handler(Looper.getMainLooper()); @Override protected void onCreate(@Nullable Bundle savedInstanceState) { @@ -155,6 +158,83 @@ public void handleOnBackPressed() { fabImport.setOnClickListener(v -> doImport()); setMetaIdle(); runOnPool(this::asyncLoad); + if (!restoreSession()) reattachActiveDownload(); + } + + /** Snapshot of the form, kept across activity re-creation while a download runs. */ + private static final class Session { + String metaSource, customMetaUrl, dlSource, customDlUrl; + List allImages; + String distro, version, variant, build; + String filename, folder; + } + + /** The in-progress import (only one download runs at a time), or {@code null}. */ + private static Session session; + + private void captureSession() { + var s = new Session(); + s.metaSource = dropdownMetaSource.getText(); + s.customMetaUrl = inputCustomMetaUrl.getText(); + s.dlSource = dropdownDlSource.getText(); + s.customDlUrl = inputCustomDlUrl.getText(); + s.allImages = new ArrayList<>(allImages); + s.distro = dropdownDistro.getText(); + s.version = dropdownVersion.getText(); + s.variant = dropdownVariant.getText(); + s.build = dropdownBuild.getText(); + s.filename = inputFilename.getText(); + s.folder = inputFolder.getText(); + session = s; + } + + /** + * Rebuilds the form from the last saved session (replays the + * distro→version→variant→build cascade) and re-attaches the progress bar if a + * download is still running. The session is kept after the download ends too, + * so reopening restores the last selections (hit Load to refresh). Returns + * false if there's nothing to restore. + */ + private boolean restoreSession() { + var s = session; + if (s == null) return false; + if (lxcRepo == null) return false; // sources unavailable; can't rebuild + dropdownMetaSource.setText(s.metaSource); + inputCustomMetaUrl.setText(s.customMetaUrl); + inputCustomMetaUrl.setVisibility( + getSelectedMetaSourceKey().equals("custom") ? VISIBLE : GONE); + dropdownDlSource.setText(s.dlSource); + inputCustomDlUrl.setText(s.customDlUrl); + inputCustomDlUrl.setVisibility( + getSelectedDlSourceKey().equals("custom") ? VISIBLE : GONE); + onImagesLoaded(s.allImages, s.allImages.size()); + dropdownDistro.setText(s.distro); + onDistroSelected(s.distro); + dropdownVersion.setText(s.version); + onVersionSelected(s.version); + dropdownVariant.setText(s.variant); + onVariantSelected(s.variant); + dropdownBuild.setText(s.build); + onBuildSelected(s.build); + inputFilename.setText(s.filename); + inputFolder.setText(s.folder); + reattachActiveDownload(); + return true; + } + + /** Re-attaches just the progress bar to the running download (no form state). */ + private void reattachActiveDownload() { + long id = DiskDownloadManager.activeDownloadId(getClass().getName()); + if (id < 0) return; + currentDownloadId = id; + isDownloading = true; + setInputsEnabled(false); + fabImport.setVisibility(GONE); + downloadWidget.setVisibility(VISIBLE); + var name = DiskDownloadManager.downloadName(id); + downloadWidget.startExternal(name != null ? name : "", this::cancelDownload); + scrollView.post(() -> scrollView.fullScroll(View.FOCUS_DOWN)); + pollHandler.post(pollRunnable); } private void asyncLoad() { @@ -179,21 +259,18 @@ private void afterApiDone() { @Override protected void onDestroy() { super.onDestroy(); - if (downloadWidget != null) - downloadWidget.stopAndCleanup(); + // Stop driving the on-screen widget; the download keeps running in the + // foreground service (notification shade) and registers the disk itself. + pollHandler.removeCallbacks(pollRunnable); } private void confirmExit() { - if (isDownloading && downloadWidget != null && downloadWidget.isRunning()) { - new MaterialAlertDialogBuilder(this) - .setTitle(R.string.download_exit_title) - .setMessage(R.string.download_exit_message) - .setNegativeButton(android.R.string.cancel, null) - .setPositiveButton(android.R.string.ok, (d, w) -> finish()) - .show(); - } else { - finish(); - } + // Leave normally (so you can navigate to other screens while downloading). + // The download keeps running in the foreground service; its state is + // restored when this screen is re-opened. + if (isDownloading && currentDownloadId >= 0) + Toast.makeText(this, R.string.download_background_toast, LENGTH_SHORT).show(); + finish(); } private void onFolderPickerResult(Uri uri) { @@ -486,6 +563,10 @@ private void hideOutput() { private void doImport() { if (isDownloading) return; + if (DiskDownloadManager.hasActiveDownload()) { + Toast.makeText(this, R.string.download_one_at_a_time, LENGTH_SHORT).show(); + return; + } if (selectedImage == null) { Toast.makeText( this, R.string.lxc_error_no_image, @@ -520,58 +601,118 @@ private void doImport() { } downloadName = name; downloadFolder = folder; - startDownload(downloadUrl, destPath); + startDownload(downloadUrl); } - private void startDownload(String url, String destPath) { + private void startDownload(String url) { isDownloading = true; + captureSession(); setInputsEnabled(false); fabImport.setVisibility(GONE); - downloadWidget.setUserAgent(LXC_USER_AGENT); downloadWidget.setVisibility(VISIBLE); scrollView.post(() -> scrollView.fullScroll(View.FOCUS_DOWN)); - downloadWidget.setOnDownloadListener(this); - downloadWidget.start(url, destPath); + downloadWidget.startExternal(downloadName, this::cancelDownload); + final var folder = downloadFolder; + final var name = downloadName; + // enqueue() resolves redirects (network I/O), so run it off the main thread. + runOnPool(() -> { + long id = DiskDownloadManager.enqueue( + this, url, LXC_USER_AGENT, folder, name, ImportLxcImagesActivity.class); + runOnUiThread(() -> onDownloadEnqueued(id)); + }); } - @Override - public void onFinished(DownloadWidget widget, File file) { - var config = new DiskConfig(); - config.setName(downloadName); - config.item.set("folder", downloadFolder); - Runnable done = () -> { - var resultData = new Intent(); - resultData.putExtra("result_disk_path", config.getFullPath()); - setResult(RESULT_OK, resultData); - if (config.getFormat() == DiskFormat.QCOW2) - startOptimize(this, config.getId()); + private void onDownloadEnqueued(long id) { + if (id < 0) { + if (isDownloading) onDownloadFailed(getString(R.string.download_error_start)); + return; + } + if (!isDownloading) { + // Cancelled while still enqueueing. + DiskDownloadManager.cancel(id); + return; + } + currentDownloadId = id; + DiskDownloadService.start(this, id); + if (!isDestroyed()) pollHandler.post(pollRunnable); + } + + /** Mirrors the download's live state into the on-screen widget. */ + private final Runnable pollRunnable = new Runnable() { + @Override + public void run() { + if (currentDownloadId < 0) return; + var p = DiskDownloadManager.query(currentDownloadId); + if (p == null) { + cancelDownload(); // job gone (cancelled elsewhere) + return; + } + switch (p.state) { + case DiskDownloadManager.STATE_SUCCESS: + onDownloadSucceeded(); + break; + case DiskDownloadManager.STATE_FAILED: + onDownloadFailed(p.reason); + break; + case DiskDownloadManager.STATE_CANCELLED: + cancelDownload(); + break; + case DiskDownloadManager.STATE_PAUSED: + downloadWidget.updateExternal(p.downloaded, p.total); + downloadWidget.markExternalPaused( + p.reason != null ? p.reason : getString(R.string.download_paused)); + pollHandler.postDelayed(this, POLL_INTERVAL_MS); + break; + default: // CONNECTING, RUNNING + downloadWidget.updateExternal(p.downloaded, p.total); + pollHandler.postDelayed(this, POLL_INTERVAL_MS); + break; + } + } + }; + + private void onDownloadSucceeded() { + long id = currentDownloadId; + currentDownloadId = -1; + pollHandler.removeCallbacks(pollRunnable); + downloadWidget.markExternalFinished(); + var result = DiskDownloadManager.getResult(id); + if (result == null) { finish(); - }; - runOnPool(() -> { - var store = new DiskStore(); - store.load(this); - store.add(config); - store.save(this); - runOnUiThread(done); - }); + return; + } Toast.makeText( this, - getString(R.string.lxc_import_success, downloadName), + getString(R.string.lxc_import_success, result.name), LENGTH_SHORT ).show(); + var resultData = new Intent(); + resultData.putExtra("result_disk_path", pathJoin(result.folder, result.name)); + setResult(RESULT_OK, resultData); + if (result.diskId != null && DiskFormat.fromFilename(result.name) == DiskFormat.QCOW2) + startOptimize(this, result.diskId); + finish(); } - @Override - public void onFailed(DownloadWidget widget, String error) { - isDownloading = false; - setInputsEnabled(true); - progressMeta.setVisibility(GONE); - btnLoad.setEnabled(true); - fabImport.setVisibility(VISIBLE); + private void cancelDownload() { + long id = currentDownloadId; + currentDownloadId = -1; + pollHandler.removeCallbacks(pollRunnable); + if (id >= 0) DiskDownloadManager.cancel(id); + downloadWidget.markExternalCancelled(); + resetAfterDownloadStop(); } - @Override - public void onCancelled(DownloadWidget widget) { + private void onDownloadFailed(@Nullable String reason) { + long id = currentDownloadId; + currentDownloadId = -1; + pollHandler.removeCallbacks(pollRunnable); + if (id >= 0) DiskDownloadManager.cancel(id); + downloadWidget.markExternalFailed(reason); + resetAfterDownloadStop(); + } + + private void resetAfterDownloadStop() { isDownloading = false; setInputsEnabled(true); progressMeta.setVisibility(GONE); diff --git a/app/src/main/java/cn/classfun/droidvm/ui/widgets/tools/DownloadWidget.java b/app/src/main/java/cn/classfun/droidvm/ui/widgets/tools/DownloadWidget.java index 9d9a6ec08..e17d7cec4 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/widgets/tools/DownloadWidget.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/widgets/tools/DownloadWidget.java @@ -1,16 +1,10 @@ package cn.classfun.droidvm.ui.widgets.tools; -import static cn.classfun.droidvm.lib.utils.NetUtils.USER_AGENT; -import static cn.classfun.droidvm.lib.utils.StringUtils.basename; -import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; import static cn.classfun.droidvm.lib.utils.StringUtils.formatDuration; import static cn.classfun.droidvm.lib.size.SizeUtils.formatSize; import android.content.Context; -import android.os.Handler; -import android.os.Looper; import android.util.AttributeSet; -import android.util.Log; import android.view.LayoutInflater; import android.widget.FrameLayout; import android.widget.TextView; @@ -22,34 +16,23 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.progressindicator.LinearProgressIndicator; -import java.io.Closeable; -import java.io.File; -import java.io.FileOutputStream; -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.net.URL; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.atomic.AtomicBoolean; - import cn.classfun.droidvm.R; -import cn.classfun.droidvm.lib.utils.NetUtils.HttpException; +/** + * A progress view for a download owned elsewhere (e.g. {@link android.app.DownloadManager}). + * It renders only — it never runs a download itself. Drive it from the main thread: + * call {@link #startExternal} once, then feed it {@link #updateExternal} and the + * {@code markExternal*} state transitions as the download progresses. The cancel + * button invokes the handler passed to {@link #startExternal}. + */ public final class DownloadWidget extends FrameLayout { - private static final String TAG = "DownloadWidget"; private static final int UI_UPDATE_INTERVAL_MS = 300; private final Context context; private TextView tvFilename, tvPercent, tvSpeed, tvSize, tvEta; private LinearProgressIndicator progressBar; private MaterialButton btnCancel; - private final Handler handler = new Handler(Looper.getMainLooper()); - private final ExecutorService executor = Executors.newSingleThreadExecutor(); - private final AtomicBoolean cancelled = new AtomicBoolean(false); - private String url; - private String destPath; private String fileName; - private String userAgent = USER_AGENT; - private volatile int state = STATE_IDLE; + private int state = STATE_IDLE; public static final int STATE_IDLE = 0; public static final int STATE_CONNECTING = 1; public static final int STATE_DOWNLOADING = 2; @@ -61,25 +44,7 @@ public final class DownloadWidget extends FrameLayout { private long speedBytesPerSec = 0; private long lastUpdateTime = 0; private long lastUpdateBytes = 0; - private OnDownloadListener listener; - - @SuppressWarnings({"EmptyMethod", "unused"}) - public interface OnDownloadListener { - default void onStateChanged(DownloadWidget widget, int state) { - } - - default void onProgress(DownloadWidget widget, long downloaded, long total) { - } - - default void onFinished(DownloadWidget widget, File file) { - } - - default void onFailed(DownloadWidget widget, String error) { - } - - default void onCancelled(DownloadWidget widget) { - } - } + private Runnable cancelHandler; public DownloadWidget(@NonNull Context context) { super(context); @@ -113,16 +78,13 @@ private void init() { setIdle(); } - public void setOnDownloadListener(OnDownloadListener listener) { - this.listener = listener; - } - - public void start(@NonNull String url, @NonNull String destPath) { - if (state == STATE_CONNECTING || state == STATE_DOWNLOADING) return; - this.url = url; - this.destPath = destPath; - this.fileName = basename(destPath); - this.cancelled.set(false); + /** + * Begins showing progress for a download owned elsewhere. The cancel button + * invokes {@code onCancel}. + */ + public void startExternal(@NonNull String fileName, @NonNull Runnable onCancel) { + this.cancelHandler = onCancel; + this.fileName = fileName; this.downloadedBytes = 0; this.totalBytes = -1; this.speedBytesPerSec = 0; @@ -130,29 +92,47 @@ public void start(@NonNull String url, @NonNull String destPath) { this.lastUpdateBytes = 0; tvFilename.setText(fileName); setState(STATE_CONNECTING); - executor.submit(this::doDownload); } - public void setUserAgent(String userAgent) { - this.userAgent = userAgent; + /** Feeds the latest byte counts from the external download into the UI. */ + public void updateExternal(long downloaded, long total) { + if (state == STATE_FINISHED || state == STATE_FAILED || state == STATE_CANCELLED) return; + if (state != STATE_DOWNLOADING) setState(STATE_DOWNLOADING); + totalBytes = total; + long now = System.currentTimeMillis(); + if (lastUpdateTime == 0) { + lastUpdateTime = now; + lastUpdateBytes = downloaded; + } + long elapsed = now - lastUpdateTime; + if (elapsed >= UI_UPDATE_INTERVAL_MS) { + long deltaBytes = downloaded - lastUpdateBytes; + speedBytesPerSec = elapsed > 0 ? (deltaBytes * 1000 / elapsed) : 0; + lastUpdateTime = now; + lastUpdateBytes = downloaded; + } + downloadedBytes = downloaded; + updateUI(downloaded, total, speedBytesPerSec); + } + + /** Renders the download as paused, showing why (waiting for network, retrying, …). */ + public void markExternalPaused(@NonNull String reasonText) { + if (state == STATE_FINISHED) return; + progressBar.setIndeterminate(false); + tvSpeed.setText(""); + tvEta.setText(reasonText); } - public boolean isRunning() { - return state == STATE_CONNECTING || state == STATE_DOWNLOADING; + public void markExternalFinished() { + setState(STATE_FINISHED); } - public void stopAndCleanup() { - if (!isRunning()) return; - cancelled.set(true); + public void markExternalFailed(@Nullable String msg) { + setFailed(msg != null ? msg : "Unknown error"); } - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - if (isRunning()) { - cancelled.set(true); - } - executor.shutdownNow(); + public void markExternalCancelled() { + setState(STATE_CANCELLED); } private void showCancelConfirmation() { @@ -162,99 +142,12 @@ private void showCancelConfirmation() { .setTitle(R.string.download_cancel_title) .setMessage(ctx.getString(R.string.download_cancel_message, fileName)) .setNegativeButton(android.R.string.cancel, null) - .setPositiveButton(android.R.string.ok, (d, w) -> doCancel()) + .setPositiveButton(android.R.string.ok, (d, w) -> { + if (cancelHandler != null) cancelHandler.run(); + }) .show(); } - private void doCancel() { - cancelled.set(true); - } - - private void doDownload() { - InputStream in = null; - FileOutputStream out = null; - HttpURLConnection conn = null; - var destFile = new File(destPath); - try { - var parent = destFile.getParentFile(); - if (parent != null && !parent.exists() && !parent.mkdirs()) - Log.w(TAG, fmt("Failed to create parent directories: %s", parent.getAbsolutePath())); - Log.i(TAG, fmt("Starting download: %s", url)); - conn = (HttpURLConnection) new URL(url).openConnection(); - conn.setConnectTimeout(30000); - conn.setReadTimeout(30000); - conn.setInstanceFollowRedirects(true); - conn.setRequestProperty("User-Agent", userAgent); - int code = conn.getResponseCode(); - if (code < 200 || code >= 400) - throw new HttpException(code); - totalBytes = conn.getContentLengthLong(); - handler.post(() -> setState(STATE_DOWNLOADING)); - var contType = conn.getContentType(); - if (contType != null && contType.contains("text/html")) - throw new RuntimeException("Download URL points to an HTML page, not a file"); - in = conn.getInputStream(); - out = new FileOutputStream(destFile); - var buffer = new byte[65536]; - int bytesRead; - lastUpdateTime = System.currentTimeMillis(); - lastUpdateBytes = 0; - while ((bytesRead = in.read(buffer)) != -1) { - if (cancelled.get()) { - handler.post(() -> { - setState(STATE_CANCELLED); - if (listener != null) listener.onCancelled(this); - }); - closeQuietly(out); - out = null; - if (destFile.exists() && !destFile.delete()) Log.w(TAG, fmt( - "Failed to delete incomplete file: %s", - destFile.getAbsolutePath() - )); - return; - } - out.write(buffer, 0, bytesRead); - downloadedBytes += bytesRead; - long now = System.currentTimeMillis(); - if (now - lastUpdateTime >= UI_UPDATE_INTERVAL_MS) { - long elapsed = now - lastUpdateTime; - long deltaBytes = downloadedBytes - lastUpdateBytes; - speedBytesPerSec = elapsed > 0 ? (deltaBytes * 1000 / elapsed) : 0; - lastUpdateTime = now; - lastUpdateBytes = downloadedBytes; - final long dl = downloadedBytes; - final long total = totalBytes; - final long speed = speedBytesPerSec; - handler.post(() -> updateUI(dl, total, speed)); - } - } - out.flush(); - closeQuietly(out); - out = null; - handler.post(() -> { - setState(STATE_FINISHED); - if (listener != null) listener.onFinished(this, destFile); - }); - } catch (Exception e) { - Log.e(TAG, "Download failed", e); - String msg = e.getMessage(); - closeQuietly(out); - out = null; - if (destFile.exists() && !destFile.delete()) Log.w(TAG, fmt( - "Failed to delete incomplete file: %s", - destFile.getAbsolutePath() - )); - handler.post(() -> { - setFailed(msg != null ? msg : "Unknown error"); - if (listener != null) listener.onFailed(this, msg); - }); - } finally { - closeQuietly(in); - closeQuietly(out); - if (conn != null) conn.disconnect(); - } - } - private void setIdle() { tvFilename.setText(""); tvPercent.setText(""); @@ -307,7 +200,6 @@ private void setState(int newState) { case STATE_FAILED: break; } - if (listener != null) listener.onStateChanged(this, newState); } private void setFailed(String msg) { @@ -318,12 +210,14 @@ private void setFailed(String msg) { tvSize.setText(""); tvEta.setText(getContext().getString(R.string.download_failed, msg)); btnCancel.setVisibility(GONE); - if (listener != null) listener.onStateChanged(this, STATE_FAILED); } private void updateUI(long downloaded, long total, long speed) { var ctx = getContext(); if (total > 0) { + // Material's setProgress() is ignored while indeterminate, so leave that + // mode explicitly (e.g. after a retry where the size was briefly unknown). + progressBar.setIndeterminate(false); int progress = (int) (downloaded * 1000 / total); progressBar.setProgress(progress); float percent = downloaded * 100f / total; @@ -365,15 +259,5 @@ private void updateUI(long downloaded, long total, long speed) { ctx.getString(R.string.download_eta_unknown) )); } - if (listener != null) listener.onProgress(this, downloaded, total); - } - - private static void closeQuietly(Closeable c) { - if (c != null) { - try { - c.close(); - } catch (Exception ignored) { - } - } } } diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 650cb176b..9d0c8aaf9 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -615,6 +615,25 @@ 文件图标 正在下载 下载仍在进行中。现在离开将取消下载并删除已下载的部分文件。确定要退出吗? + 已暂停… + 已暂停,等待网络… + 连接中断,正在重试… + 已暂停,等待 Wi-Fi… + 连接中断,重试中 (%1$d/%2$d)… + 已有下载正在进行,请等待完成。 + 下载将在后台继续,可在通知栏查看。 无法开始下载,请确认系统下载管理器已启用。 + + 下载 + 磁盘镜像下载通知 + 导入完成 + "%1$s"已添加到磁盘列表。 + 下载失败 + 无法下载"%1$s"。 + 下载中 + 下载进行中的通知 + 连接中… + 取消 + 已挂起 虚拟机已挂起 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2faf73b8c..116ff15b8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -652,6 +652,25 @@ File icon Download in progress A download is still in progress. Leaving now will cancel it and delete the partial file. Are you sure? + Paused… + Paused, waiting for network… + Connection interrupted, retrying… + Paused, waiting for Wi-Fi… + Connection lost, retrying (%1$d/%2$d)… + A download is already in progress. Please wait for it to finish. + Download continues in the background. Check the notification shade. Could not start the download. Make sure the system Download Manager is enabled. + + Downloads + Disk image download notifications + Import complete + \"%1$s\" was added to the disk list. + Download failed + Failed to download \"%1$s\". + Downloading + Ongoing download progress + Connecting… + Cancel + Suspended VM is suspended From 5c019ac7311c260e7ad482779f2e3a4404d10797 Mon Sep 17 00:00:00 2001 From: HuJK Date: Sun, 14 Jun 2026 16:39:11 +0800 Subject: [PATCH 012/182] fix(ui): handle IME insets and LXC source loading Apply IME inset padding globally while exempting console/display surfaces, preventing the soft keyboard from covering editor UI. Make the LXC import screen usable immediately from bundled source metadata, then refresh mirrors in the background without clobbering an explicit user selection. Co-authored-by: Claude Opus 4.8 --- .../java/cn/classfun/droidvm/DroidVMApp.java | 2 + .../droidvm/lib/ui/ImeInsetsApplier.java | 81 +++++++++++ .../droidvm/lib/ui/ImeInsetsExempt.java | 11 ++ .../ui/disk/lxc/ImportLxcImagesActivity.java | 127 +++++++++++++++--- .../ui/vm/console/VMConsoleActivity.java | 3 +- .../vm/display/vnc/base/BaseVncActivity.java | 3 +- app/src/main/res/values-zh-rCN/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 8 files changed, 210 insertions(+), 19 deletions(-) create mode 100644 app/src/main/java/cn/classfun/droidvm/lib/ui/ImeInsetsApplier.java create mode 100644 app/src/main/java/cn/classfun/droidvm/lib/ui/ImeInsetsExempt.java diff --git a/app/src/main/java/cn/classfun/droidvm/DroidVMApp.java b/app/src/main/java/cn/classfun/droidvm/DroidVMApp.java index 43fc13809..4d6a266eb 100644 --- a/app/src/main/java/cn/classfun/droidvm/DroidVMApp.java +++ b/app/src/main/java/cn/classfun/droidvm/DroidVMApp.java @@ -11,6 +11,7 @@ import cn.classfun.droidvm.lib.store.disk.DiskStore; import cn.classfun.droidvm.lib.store.network.NetworkStore; import cn.classfun.droidvm.lib.store.vm.VMStore; +import cn.classfun.droidvm.lib.ui.ImeInsetsApplier; import cn.classfun.droidvm.lib.utils.ThreadUtils; public final class DroidVMApp extends Application { @@ -21,6 +22,7 @@ public final class DroidVMApp extends Application { public void onCreate() { super.onCreate(); DynamicColors.applyToActivitiesIfAvailable(this); + registerActivityLifecycleCallbacks(new ImeInsetsApplier()); vmEventHandler = new VMEventHandler(this); registerActivityLifecycleCallbacks(vmEventHandler); DaemonConnection.getInstance().addListener(vmEventHandler); diff --git a/app/src/main/java/cn/classfun/droidvm/lib/ui/ImeInsetsApplier.java b/app/src/main/java/cn/classfun/droidvm/lib/ui/ImeInsetsApplier.java new file mode 100644 index 000000000..948b49525 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/lib/ui/ImeInsetsApplier.java @@ -0,0 +1,81 @@ +package cn.classfun.droidvm.lib.ui; + +import android.app.Activity; +import android.app.Application; +import android.os.Bundle; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.graphics.Insets; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; + +/** + * Reinstates the legacy {@code adjustResize} behaviour under edge-to-edge. + * + *

From {@code targetSdk} 35 the app always draws edge-to-edge, and the + * framework no longer shrinks the window for the soft keyboard; instead it + * reports the keyboard height as an {@link WindowInsetsCompat.Type#ime() ime} + * inset that the app must consume. {@code android:fitsSystemWindows} only + * applies the system bar insets, so without this the keyboard covers whatever + * the user is typing into. + * + *

This applier pads the bottom of every activity's content view by the IME + * inset, lifting the content (and any scroll container within it) above the + * keyboard for every activity uniformly. Activities that handle the keyboard + * themselves opt out via {@link ImeInsetsExempt}. + */ +public final class ImeInsetsApplier implements Application.ActivityLifecycleCallbacks { + + private static void apply(Activity activity) { + if (activity instanceof ImeInsetsExempt) return; + final View content = activity.findViewById(android.R.id.content); + if (content == null) return; + final int basePadding = content.getPaddingBottom(); + ViewCompat.setOnApplyWindowInsetsListener(content, (v, insets) -> { + final Insets ime = insets.getInsets(WindowInsetsCompat.Type.ime()); + v.setPadding( + v.getPaddingLeft(), + v.getPaddingTop(), + v.getPaddingRight(), + basePadding + ime.bottom + ); + return insets; + }); + ViewCompat.requestApplyInsets(content); + } + + @Override + public void onActivityPostCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) { + apply(activity); + } + + @Override + public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) { + } + + @Override + public void onActivityStarted(@NonNull Activity activity) { + } + + @Override + public void onActivityResumed(@NonNull Activity activity) { + } + + @Override + public void onActivityPaused(@NonNull Activity activity) { + } + + @Override + public void onActivityStopped(@NonNull Activity activity) { + } + + @Override + public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) { + } + + @Override + public void onActivityDestroyed(@NonNull Activity activity) { + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/lib/ui/ImeInsetsExempt.java b/app/src/main/java/cn/classfun/droidvm/lib/ui/ImeInsetsExempt.java new file mode 100644 index 000000000..a78ec638e --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/lib/ui/ImeInsetsExempt.java @@ -0,0 +1,11 @@ +package cn.classfun.droidvm.lib.ui; + +/** + * Marker for activities that manage the soft keyboard themselves (e.g. the + * terminal/serial console and VNC display, where the IME must overlay the + * full-screen surface rather than resize it). + * + *

{@link ImeInsetsApplier} skips any activity implementing this interface. + */ +public interface ImeInsetsExempt { +} diff --git a/app/src/main/java/cn/classfun/droidvm/ui/disk/lxc/ImportLxcImagesActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/disk/lxc/ImportLxcImagesActivity.java index 5779c7a09..9df72c2c7 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/disk/lxc/ImportLxcImagesActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/disk/lxc/ImportLxcImagesActivity.java @@ -98,6 +98,9 @@ public final class ImportLxcImagesActivity extends AppCompatActivity { private boolean isLoading = false; private boolean isDownloading = false; private boolean isClassFunApiAvailable = false; + /** True once the user explicitly picks a source, so a background refresh + * won't override their choice (but may still upgrade an untouched default). */ + private boolean userTouchedSource = false; private String downloadName = null; private String downloadFolder = null; private ApiManager apiManager = null; @@ -156,8 +159,12 @@ public void handleOnBackPressed() { inputFolder.setText(path); inputFolder.setIconButtonOnClickListener(() -> folderPickerLauncher.launch(null)); fabImport.setOnClickListener(v -> doImport()); - setMetaIdle(); - runOnPool(this::asyncLoad); + // show the built-in source list immediately (screen usable at once), + // then refresh from the API in the background and swap it in when ready + loadBuiltinSources(); + runOnPool(this::asyncRefreshSources); + // If this screen was re-created while its download is still running, + // restore the whole form; otherwise just re-attach the progress bar. if (!restoreSession()) reattachActiveDownload(); } @@ -237,23 +244,102 @@ private void reattachActiveDownload() { pollHandler.post(pollRunnable); } - private void asyncLoad() { - if (Privacy.isPrivacyAgreed(this)) { - apiManager = ApiManager.create(this); - isClassFunApiAvailable = apiManager.isServiceEnabled("lxc_images_metadata"); + /** + * Populates the source dropdowns synchronously from the bundled repo list + * so the screen is usable immediately, before the API refresh returns. + */ + private void loadBuiltinSources() { + var repos = Repos.loadYAML(this); + if (repos != null) lxcRepo = repos.getRepo().get("lxc-images"); + if (lxcRepo == null) { + // bundled data missing/corrupt: fall back to a blocking load + setMetaSourcesLoading(); + return; } - var repos = Repos.load(this); - if (repos == null) - throw new IllegalStateException("Repo data not found or failed to load"); - lxcRepo = repos.getRepo().get("lxc-images"); - if (lxcRepo == null) - throw new IllegalStateException("LXC repo info not found in the data"); - runOnUiThread(this::afterApiDone); - } - - private void afterApiDone() { setupSourceDropdown(); setupImageDropdowns(); + setMetaRefreshing(); + } + + /** + * Background refresh: fetches the freshest repo/mirror list (and ClassFun + * availability) from the API, then swaps it into the dropdowns without + * disturbing a user who has already moved on to loading images. + */ + private void asyncRefreshSources() { + boolean classfun = false; + Repos.Repo freshRepo = null; + try { + if (Privacy.isPrivacyAgreed(this)) { + apiManager = ApiManager.create(this); + classfun = apiManager.isServiceEnabled("lxc_images_metadata"); + } + var repos = Repos.load(this); + if (repos != null) freshRepo = repos.getRepo().get("lxc-images"); + } catch (Exception e) { + Log.w(TAG, "Background source refresh failed; keeping built-in list", e); + } + final boolean classfunAvailable = classfun; + final Repos.Repo repo = freshRepo; + runOnUiThread(() -> applyRefreshedSources(repo, classfunAvailable)); + } + + private void applyRefreshedSources(@Nullable Repos.Repo freshRepo, boolean classfunAvailable) { + if (isFinishing()) return; + isClassFunApiAvailable = classfunAvailable; + if (freshRepo != null) { + // capture any explicit choice before the list swap + var metaSel = getSelectedMetaSourceKey(); + var dlSel = getSelectedDlSourceKey(); + lxcRepo = freshRepo; + setupSourceDropdown(); + // keep an explicit pick; otherwise take the fresh default + // (which now prefers ClassFun when it just became available) + if (userTouchedSource) restoreSourceSelection(metaSel, dlSel); + } else if (lxcRepo == null) { + // both the bundled list and the refresh failed + setMetaError("source list unavailable"); + btnLoad.setEnabled(false); + return; + } + // clear the "refreshing" spinner only if the user hasn't moved on + if (!isLoading && !isDownloading && allImages.isEmpty()) + setMetaIdle(); + } + + /** Re-applies a previously selected source key after the list is rebuilt. */ + private void restoreSourceSelection(@NonNull String metaKey, @NonNull String dlKey) { + int mi = findSourceIndex(metaSourceKeys, metaKey); + dropdownMetaSource.setText(metaSourceLabels[mi]); + inputCustomMetaUrl.setVisibility( + metaSourceKeys[mi].equals("custom") ? VISIBLE : GONE); + int di = findSourceIndex(dlSourceKeys, dlKey); + dropdownDlSource.setText(dlSourceLabels[di]); + inputCustomDlUrl.setVisibility( + dlSourceKeys[di].equals("custom") ? VISIBLE : GONE); + } + + private void setMetaRefreshing() { + progressMeta.setVisibility(VISIBLE); + tvMetaStatus.setText(R.string.lxc_meta_refreshing); + tvMetaStatus.setTextColor(resolveThemeColor(colorOnSurfaceVariant)); + btnLoad.setEnabled(true); + setImageSectionEnabled(false); + setOutputEnabled(false); + } + + private void setMetaSourcesLoading() { + progressMeta.setVisibility(VISIBLE); + tvMetaStatus.setText(R.string.lxc_meta_loading); + tvMetaStatus.setTextColor(resolveThemeColor(colorOnSurfaceVariant)); + btnLoad.setEnabled(false); + setImageSectionEnabled(false); + setOutputEnabled(false); + } + + private boolean sourcesReady() { + return metaSourceKeys != null && metaSourceLabels != null + && dlSourceKeys != null && dlSourceLabels != null; } @Override @@ -346,6 +432,7 @@ private void setupSourceDropdown() { dropdownMetaSource.setAdapter(aMeta); dropdownMetaSource.setText(metaSourceLabels[metaDefaultIndex]); dropdownMetaSource.setOnItemClickListener((p, v, pos, id) -> { + userTouchedSource = true; boolean isCustom = metaSourceKeys[pos].equals("custom"); inputCustomMetaUrl.setVisibility(isCustom ? VISIBLE : GONE); setMetaIdle(); @@ -358,6 +445,7 @@ private void setupSourceDropdown() { dropdownDlSource.setAdapter(aDown); dropdownDlSource.setText(dlSourceLabels[downDefaultIndex]); dropdownDlSource.setOnItemClickListener((p, v, pos, id) -> { + userTouchedSource = true; boolean isCustom = dlSourceKeys[pos].equals("custom"); inputCustomDlUrl.setVisibility(isCustom ? VISIBLE : GONE); }); @@ -373,6 +461,7 @@ private int findSourceIndex(@NonNull String[] keys, @NonNull String target) { @Nullable private String resolveSourceUrl(@NonNull String key) { + if (lxcRepo == null) return null; if (key.equals("official")) return lxcRepo.getUrl(); if (key.equals("custom")) return null; var mirror = lxcRepo.getMirror(key); @@ -410,7 +499,7 @@ private String getDownloadBaseUrl() { } private void loadImages() { - if (isLoading || isDownloading) return; + if (isLoading || isDownloading || !sourcesReady()) return; var baseUrl = getMetaBaseUrl(); if (baseUrl.isEmpty()) { if (getSelectedMetaSourceKey().equals("custom")) @@ -778,6 +867,8 @@ private void setOutputEnabled(boolean enabled) { } private String getSelectedMetaSourceKey() { + if (metaSourceLabels == null || metaSourceKeys == null) + return isClassFunApiAvailable ? "classfun" : "official"; var label = dropdownMetaSource.getText(); for (int i = 0; i < metaSourceLabels.length; i++) if (metaSourceLabels[i].equals(label)) @@ -786,6 +877,8 @@ private String getSelectedMetaSourceKey() { } private String getSelectedDlSourceKey() { + if (dlSourceLabels == null || dlSourceKeys == null) + return "official"; var label = dropdownDlSource.getText(); for (int i = 0; i < dlSourceLabels.length; i++) if (dlSourceLabels[i].equals(label)) diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/console/VMConsoleActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/console/VMConsoleActivity.java index c27fb9e52..d5306c6e1 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/console/VMConsoleActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/console/VMConsoleActivity.java @@ -48,11 +48,12 @@ import cn.classfun.droidvm.R; import cn.classfun.droidvm.lib.daemon.DaemonConnection; +import cn.classfun.droidvm.lib.ui.ImeInsetsExempt; import cn.classfun.droidvm.lib.ui.termux.SimpleTerminalSessionClient; import cn.classfun.droidvm.lib.ui.termux.SimpleTerminalViewClient; import cn.classfun.droidvm.lib.utils.ShareUtils; -public final class VMConsoleActivity extends AppCompatActivity { +public final class VMConsoleActivity extends AppCompatActivity implements ImeInsetsExempt { private static final String TAG = "VMConsoleActivity"; public static final String EXTRA_VM_ID = "vm_id"; public static final String EXTRA_VM_NAME = "vm_name"; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/base/BaseVncActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/base/BaseVncActivity.java index 267845f08..9331f1d0a 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/base/BaseVncActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/base/BaseVncActivity.java @@ -47,10 +47,11 @@ import cn.classfun.droidvm.R; import cn.classfun.droidvm.lib.daemon.DaemonConnection; +import cn.classfun.droidvm.lib.ui.ImeInsetsExempt; import cn.classfun.droidvm.ui.vm.display.base.DisplayExtraKeysPanel; import cn.classfun.droidvm.ui.vm.display.vnc.input.VncExtraKeysPanel; -public abstract class BaseVncActivity extends AppCompatActivity { +public abstract class BaseVncActivity extends AppCompatActivity implements ImeInsetsExempt { protected final String TAG = getClass().getSimpleName(); public static final String EXTRA_VM_NAME = "vm_name"; public static final String EXTRA_VM_ID = "vm_id"; diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 9d0c8aaf9..febfca891 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -469,6 +469,7 @@ 下载镜像站基础 URL 请先加载元数据 加载中\u2026 + 正在更新源列表\u2026 元数据:%1$d 个镜像 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 116ff15b8..5e8280518 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -505,6 +505,7 @@ Download mirror base URL Please load metadata first Loading\u2026 + Updating source list\u2026 Metadata: %1$d image Metadata: %1$d images From 64f5cbe3664cab77b8f6858207ba914fdaa3d4ea Mon Sep 17 00:00:00 2001 From: HuJK Date: Sun, 14 Jun 2026 16:40:03 +0800 Subject: [PATCH 013/182] chore(build): add third-party tool build pipeline Add a one-shot build script for submodules, third-party networking/boot tools, and APK output, and ignore generated third-party checkouts and staged binaries. Teach the app build and asset extraction path about the new gvswitch, pbridge, bridgedhcp, lbx, and netbox binaries, including lbx JNI packaging for UI-side execution. Co-authored-by: Claude Opus 4.8 --- .gitignore | 9 + app/build.gradle.kts | 57 +++++- .../cn/classfun/droidvm/lib/Constants.java | 1 + .../droidvm/lib/utils/AssetUtils.java | 1 + build.sh | 163 ++++++++++++++++++ 5 files changed, 230 insertions(+), 1 deletion(-) create mode 100755 build.sh diff --git a/.gitignore b/.gitignore index 93653b763..58b0eb928 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,12 @@ .externalNativeBuild .cxx local.properties + +# build.sh stages the compiled network/boot binaries next to the committed +# 7za prebuilt; only 7za is tracked, the rest are build output. +/app/src/thirdparty +/app/src/main/assets/bin/*/bridgedhcp +/app/src/main/assets/bin/*/gvswitch +/app/src/main/assets/bin/*/lbx +/app/src/main/assets/bin/*/netbox +/app/src/main/assets/bin/*/pbridge diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 27bfebacf..da565c190 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -16,10 +16,19 @@ fun runGit(vararg args: String): String { return output } +// Tolerant variant: returns null instead of throwing, for git calls that may +// legitimately fail (e.g. describe on a checkout with no tags). +fun runGitOrNull(vararg args: String): String? = + runCatching { runGit(*args) }.getOrNull() + val gitCommitCount = runGit("rev-list", "--count", "HEAD").toInt() val gitShortSha = runGit("rev-parse", "--short", "HEAD") -val gitDescribe = runGit("describe", "--long", "--tags") +// describe fails on a checkout without tags (shallow clone, or tags not fetched); +// fall back to a 0.0.0 base so the build still works and the version is clearly +// marked as tag-less. +val gitDescribe = (runGitOrNull("describe", "--long", "--tags") + ?: "0.0.0-$gitCommitCount-g$gitShortSha") .removePrefix("v").removePrefix("V") val generatedVersionName: String = if (gitDescribe.matches(Regex(".*-0-g[0-9a-f]+$"))) { gitDescribe.replace(Regex("-0-g[0-9a-f]+$"), "") @@ -86,6 +95,14 @@ android { aidl = true buildConfig = true } + packaging { + jniLibs { + // Extract native libs to a real on-disk dir so lbx (shipped as + // liblbx.so) is an executable file the app can run from its own + // nativeLibraryDir — no root, no daemon needed for a URL fetch. + useLegacyPackaging = true + } + } } abstract class CopyNativeBinAssetsTask : DefaultTask() { @@ -122,9 +139,47 @@ abstract class CopyNativeBinAssetsTask : DefaultTask() { } } +// Ship lbx as `liblbx.so` in jniLibs so the (non-root) UI process can exec it +// from its nativeLibraryDir — the data-dir copy used by the daemon isn't +// executable by the app's untrusted_app SELinux domain. +abstract class CopyLbxJniLibsTask : DefaultTask() { + @get:InputDirectory + abstract val distDir: DirectoryProperty + + @get:OutputDirectory + abstract val outputDir: DirectoryProperty + + @TaskAction + fun copy() { + val outDir = outputDir.get().asFile + outDir.deleteRecursively() + val dist = distDir.get().asFile + if (!dist.exists()) return + dist.listFiles()?.filter { it.isDirectory }?.forEach { abiDir -> + val src = File(abiDir, "lbx") + if (src.isFile) { + val dest = File(outDir, "${abiDir.name}/liblbx.so") + dest.parentFile.mkdirs() + src.copyTo(dest, overwrite = true) + } + } + } +} + androidComponents { onVariants { variant -> val variantName = variant.name.replaceFirstChar { it.uppercase() } + val copyLbxJniTask = tasks.register( + "copyLbxJniLibs${variantName}" + ) { + distDir.set(rootProject.layout.projectDirectory.dir("app/src/main/assets/bin")) + outputDir.set( + layout.buildDirectory.dir("generated/lbx_jnilibs/${variant.name}") + ) + } + variant.sources.jniLibs?.addGeneratedSourceDirectory( + copyLbxJniTask, CopyLbxJniLibsTask::outputDir + ) val copyNativeTask = tasks.register( "copyNativeBinAssets${variantName}" ) { diff --git a/app/src/main/java/cn/classfun/droidvm/lib/Constants.java b/app/src/main/java/cn/classfun/droidvm/lib/Constants.java index 4670a715e..3e8d08d65 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/Constants.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/Constants.java @@ -22,6 +22,7 @@ public final class Constants { public static final String GITHUB_ISSUE_URL = "https://github.com/Droid-VM/DroidVM/issues"; public static final String[] BINARIES_PREBUILT = {"7za"}; public static final String[] BINARIES_BUILT = {"droidvm", "daemon"}; + public static final String[] BINARIES_THIRDPARTY = {"gvswitch", "pbridge", "bridgedhcp", "lbx", "netbox"}; public static final String[] LIBRARIES_BUILT = {"libsimpledump.so", "libunixhelper.so"}; /** * IFA_RT_PRIORITY magic that pbridge tags its ND/ARP-offload proxy diff --git a/app/src/main/java/cn/classfun/droidvm/lib/utils/AssetUtils.java b/app/src/main/java/cn/classfun/droidvm/lib/utils/AssetUtils.java index b87b9da85..4a79b5738 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/utils/AssetUtils.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/utils/AssetUtils.java @@ -197,6 +197,7 @@ public static void extractAsset( public static void extractBinaries(@NonNull Context context) { extractAsset(context, "bin", BINARIES_BUILT, false); extractAsset(context, "bin", BINARIES_PREBUILT, true); + extractAsset(context, "bin", BINARIES_THIRDPARTY, false); } public static void extractLibraries(@NonNull Context context) { diff --git a/build.sh b/build.sh new file mode 100755 index 000000000..8794ab536 --- /dev/null +++ b/build.sh @@ -0,0 +1,163 @@ +#!/usr/bin/env bash +# One-shot build: submodules + third-party network daemons + APK. +# +# ./build.sh # debug APK (default) +# ./build.sh release # release APK (needs signing config) +# SKIP_THIRDPARTY=1 ./build.sh # reuse already-built binaries in app/src/main/assets/bin +# +# Third-party toolchains (Go for gvswitch/bridgedhcp, rustup+clang for +# pbridge, rustup for lbx) are only required when rebuilding those daemons; +# if a toolchain is missing but a previously built binary exists in +# app/src/main/assets/bin, the build continues with a warning. Those binaries +# are git-ignored (only the 7za prebuilt is committed); a clean checkout must +# build them at least once. +set -euo pipefail +cd "$(dirname "$0")" + +VARIANT="${1:-debug}" +case "$VARIANT" in + debug) GRADLE_TASK=assembleDebug; APK=app/build/outputs/apk/debug/app-debug.apk ;; + release) GRADLE_TASK=assembleRelease; APK=app/build/outputs/apk/release/app-release.apk ;; + *) echo "usage: $0 [debug|release]" >&2; exit 2 ;; +esac + +log() { printf '\033[1;34m==>\033[0m %s\n' "$*"; } +warn() { printf '\033[1;33mWARN:\033[0m %s\n' "$*" >&2; } +die() { printf '\033[1;31mERROR:\033[0m %s\n' "$*" >&2; exit 1; } + +# ----------------------------------------------------- environment preflight +# Required to build from a clean checkout: +# Android SDK -> ANDROID_HOME / ANDROID_SDK_ROOT, or local.properties (sdk.dir=...) +# Go (golang) -> gvswitch, bridgedhcp https://go.dev/dl/ +# Rust (cargo) -> pbridge, lbx, netbox (+ clang for pbridge) https://rustup.rs +# Go/Rust/clang are only needed when (re)building those daemons; if a toolchain +# is missing but a prebuilt binary already exists in app/src/main/assets/bin the +# build continues with a warning. A missing Android SDK is always fatal. +have() { command -v "$1" >/dev/null 2>&1; } +if [ -f local.properties ]; then SDK_STATUS="local.properties"; else SDK_STATUS="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-MISSING}}"; fi +log "Environment preflight:" +printf ' %-14s %s\n' "Android SDK" "$SDK_STATUS" +printf ' %-14s %s\n' "Go (golang)" "$(have go && go version | awk '{print $3}' || echo 'MISSING -> https://go.dev/dl/')" +printf ' %-14s %s\n' "Rust (cargo)" "$(have cargo && cargo --version | awk '{print $2}' || echo 'MISSING -> https://rustup.rs')" +printf ' %-14s %s\n' "clang" "$(have clang && echo present || echo 'MISSING -> needed for pbridge')" + +if [ ! -f local.properties ] && [ -z "${ANDROID_HOME:-}" ] && [ -z "${ANDROID_SDK_ROOT:-}" ]; then + die "Android SDK not configured: set ANDROID_HOME (or ANDROID_SDK_ROOT), or create local.properties (sdk.dir=...)" +fi + +# ---------------------------------------------------------------- submodules +log "Initializing git submodules" +git submodule update --init --recursive + +# ----------------------------------------------------- third-party repos +# These network daemons live in their own git repos (not submodules while +# under active development). Clone them on first build if the dir is missing; +# an existing checkout is left untouched so local work is never overwritten. +clone_if_missing() { + local path="$1" url="$2" + [ -d "$path/.git" ] && return 0 + if [ -e "$path" ]; then + warn "$path exists but is not a git checkout; leaving it untouched" + return 0 + fi + log "Cloning $path" + git clone "$url" "$path" +} + +clone_if_missing app/src/thirdparty/gvisor-vswitch https://github.com/HuJK/gvisor-vswitch.git +clone_if_missing app/src/thirdparty/pseudo-bridge-rs https://github.com/KusakabeShi/pseudo-bridge-rs.git +clone_if_missing app/src/thirdparty/bridgedhcp https://github.com/HuJK/bridgedhcp.git +clone_if_missing app/src/thirdparty/linux-boot-extractor https://github.com/HuJK/linux-boot-extractor.git + +# ------------------------------------------------------- third-party daemons +mkdir -p app/src/main/assets/bin/arm64-v8a app/src/main/assets/bin/x86_64 + +build_gvswitch() { + if ! command -v go >/dev/null 2>&1; then + if [ -x app/src/main/assets/bin/arm64-v8a/gvswitch ]; then + warn "Go not found; reusing the previously-built gvswitch binaries" + return 0 + fi + die "Go is required to build gvswitch (https://go.dev/dl/)" + fi + log "Building gvswitch (android-arm64 + linux-amd64)" + make -C app/src/thirdparty/gvisor-vswitch build-android build-linux-amd64 + install -m755 app/src/thirdparty/gvisor-vswitch/build/gvswitch-android-arm64 app/src/main/assets/bin/arm64-v8a/gvswitch + install -m755 app/src/thirdparty/gvisor-vswitch/build/gvswitch-linux-amd64 app/src/main/assets/bin/x86_64/gvswitch +} + +build_pbridge() { + if ! command -v cargo >/dev/null 2>&1 || ! command -v clang >/dev/null 2>&1; then + if [ -x app/src/main/assets/bin/arm64-v8a/pbridge ]; then + warn "cargo/clang not found; reusing the previously-built pbridge binaries" + return 0 + fi + die "rustup (cargo) and clang are required to build pbridge" + fi + log "Building pbridge (android-arm64 + linux-x64)" + (cd app/src/thirdparty/pseudo-bridge-rs && ./build.sh all) + install -m755 app/src/thirdparty/pseudo-bridge-rs/dist/pbridge-android-arm64 app/src/main/assets/bin/arm64-v8a/pbridge + install -m755 app/src/thirdparty/pseudo-bridge-rs/dist/pbridge-linux-x64 app/src/main/assets/bin/x86_64/pbridge +} + +build_bridgedhcp() { + if ! command -v go >/dev/null 2>&1; then + if [ -x app/src/main/assets/bin/arm64-v8a/bridgedhcp ]; then + warn "Go not found; reusing the previously-built bridgedhcp binaries" + return 0 + fi + die "Go is required to build bridgedhcp (https://go.dev/dl/)" + fi + log "Building bridgedhcp (android-arm64 + linux-amd64)" + (cd app/src/thirdparty/bridgedhcp && ./build.sh) + install -m755 app/src/thirdparty/bridgedhcp/build/bridgedhcp-android-arm64 app/src/main/assets/bin/arm64-v8a/bridgedhcp + install -m755 app/src/thirdparty/bridgedhcp/build/bridgedhcp-linux-amd64 app/src/main/assets/bin/x86_64/bridgedhcp +} + +build_lbx() { + if ! command -v cargo >/dev/null 2>&1; then + if [ -x app/src/main/assets/bin/arm64-v8a/lbx ]; then + warn "cargo not found; reusing the previously-built lbx binaries" + return 0 + fi + die "rustup (cargo) is required to build lbx" + fi + log "Building lbx (android-arm64 + linux-x64)" + (cd app/src/thirdparty/linux-boot-extractor && ./build.sh all) + install -m755 app/src/thirdparty/linux-boot-extractor/dist/lbx-android-arm64 app/src/main/assets/bin/arm64-v8a/lbx + install -m755 app/src/thirdparty/linux-boot-extractor/dist/lbx-linux-x64 app/src/main/assets/bin/x86_64/lbx +} + +build_netbox() { + if ! command -v cargo >/dev/null 2>&1; then + if [ -x app/src/main/assets/bin/arm64-v8a/netbox ]; then + warn "cargo not found; reusing the previously-built netbox binaries" + return 0 + fi + die "rustup (cargo) is required to build netbox" + fi + log "Building netbox (android-arm64 + linux-x64)" + (cd app/src/main/rust/netbox && ./build.sh all) + install -m755 app/src/main/rust/netbox/dist/netbox-android-arm64 app/src/main/assets/bin/arm64-v8a/netbox + install -m755 app/src/main/rust/netbox/dist/netbox-linux-x64 app/src/main/assets/bin/x86_64/netbox +} + +if [ "${SKIP_THIRDPARTY:-0}" = "1" ]; then + log "Skipping third-party builds (SKIP_THIRDPARTY=1)" + for bin in gvswitch pbridge bridgedhcp lbx netbox; do + [ -x "app/src/main/assets/bin/arm64-v8a/$bin" ] || die "app/src/main/assets/bin is missing $bin (run without SKIP_THIRDPARTY once)" + done +else + build_gvswitch + build_pbridge + build_bridgedhcp + build_lbx + build_netbox +fi + +# --------------------------------------------------------------------- APK +log "Building APK ($GRADLE_TASK)" +./gradlew "$GRADLE_TASK" + +[ -f "$APK" ] || die "expected APK not found: $APK" +log "Done: $APK ($(du -h "$APK" | cut -f1))" From de4e44279be502f934fd81a54289355d20db086f Mon Sep 17 00:00:00 2001 From: HuJK Date: Sun, 14 Jun 2026 19:05:03 +0800 Subject: [PATCH 014/182] feat(vm boot): preflight disk compression check crosvm can't read zlib-compressed qcow2 (guest gets vda I/O errors and no partition table, so any root= hangs). Before starting a crosvm VM, detect compressed qcow2 disks via lbx (new daemon disk_compat handler + BootPlan.hasCompressedClusters) and prompt to convert (decompress in place via DiskOperationActivity, path-mode) then auto-start; no response in 5s = start anyway. URL analysis now uses lbx 'entries --quick' to skip unused per-entry reads. UI: kernel/initrd/cmdline detection fields are read-only but selectable with a copy button; tapping the detected cmdline seeds the (now multiline) override field at the same caret offset. Co-Authored-By: Claude Opus 4.8 --- .../daemon/ipc/vm/DiskCompatHandler.java | 42 +++++ .../classfun/droidvm/daemon/vm/BootPlan.java | 18 ++ .../droidvm/lib/ui/CopyableField.java | 78 ++++++++ .../disk/operation/DiskOperationActivity.java | 81 +++++++-- .../droidvm/ui/main/vm/MainVMFragment.java | 19 +- .../cn/classfun/droidvm/ui/vm/VMActions.java | 169 ++++++++++++++++++ .../droidvm/ui/vm/boot/BootEntries.java | 7 +- .../ui/vm/edit/boot/VMEditBootTab.java | 33 ++++ .../droidvm/ui/vm/info/VMInfoActivity.java | 18 +- .../widgets/tools/KernelAnalysisWidget.java | 7 + app/src/main/res/drawable/ic_content_copy.xml | 10 ++ .../main/res/layout/partial_vm_edit_boot.xml | 6 +- .../main/res/layout/view_kernel_analysis.xml | 3 - app/src/main/res/values-zh-rCN/strings.xml | 13 +- app/src/main/res/values/strings.xml | 7 + 15 files changed, 486 insertions(+), 25 deletions(-) create mode 100644 app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/DiskCompatHandler.java create mode 100644 app/src/main/java/cn/classfun/droidvm/lib/ui/CopyableField.java create mode 100644 app/src/main/res/drawable/ic_content_copy.xml diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/DiskCompatHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/DiskCompatHandler.java new file mode 100644 index 000000000..d6805942f --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/DiskCompatHandler.java @@ -0,0 +1,42 @@ +package cn.classfun.droidvm.daemon.ipc.vm; + +import androidx.annotation.NonNull; + +import com.google.auto.service.AutoService; + +import org.json.JSONArray; + +import cn.classfun.droidvm.daemon.server.ClientRequest; +import cn.classfun.droidvm.daemon.server.RequestHandler; +import cn.classfun.droidvm.daemon.vm.BootPlan; + +/** + * Reports which of the given disk images use qcow2 features the crosvm + * backend can't read (zlib-compressed clusters) for the UI's pre-start + * guard: such an image boots to a dead end (vda I/O errors, no partition + * table, root device never appears). lbx already runs in the daemon for + * boot scans, and disk images are usually only readable here, so the check + * lives daemon-side. This is never used on the URL-analysis path. + */ +@AutoService(RequestHandler.class) +public final class DiskCompatHandler extends RequestHandler { + @NonNull + @Override + public String getName() { + return "disk_compat"; + } + + @Override + public void handle(@NonNull ClientRequest request) throws Exception { + var images = request.getParams().optJSONArray("images"); + var compressed = new JSONArray(); + if (images != null) { + for (int i = 0; i < images.length(); i++) { + var path = images.optString(i, ""); + if (!path.isEmpty() && BootPlan.hasCompressedClusters(path)) + compressed.put(path); + } + } + request.res().put("compressed", compressed); + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/BootPlan.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/BootPlan.java index ffb3fa4f6..3b6c118ae 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/BootPlan.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/BootPlan.java @@ -251,6 +251,24 @@ public static JSONArray scanEntries(@NonNull String image) throws IOException { } } + /** + * Whether {@code image} stores zlib-compressed qcow2 clusters, which the + * crosvm backend cannot read: the guest gets I/O errors and an + * unreadable partition table, so every {@code root=} form hangs waiting + * for a root device that never appears. Runs {@code lbx compat --json}; + * any lbx failure returns {@code false} so a scan hiccup never blocks a + * start (a real boot would surface the problem anyway). + */ + public static boolean hasCompressedClusters(@NonNull String image) { + try { + var out = runLbx("compat", image, "--json").trim(); + return new JSONObject(out).optBoolean("compressed_clusters", false); + } catch (Exception e) { + Log.w(TAG, fmt("compat check failed for %s: %s", image, e.getMessage())); + return false; + } + } + @Nullable private static JSONObject matchEntry( @NonNull JSONArray entries, diff --git a/app/src/main/java/cn/classfun/droidvm/lib/ui/CopyableField.java b/app/src/main/java/cn/classfun/droidvm/lib/ui/CopyableField.java new file mode 100644 index 000000000..f05284041 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/lib/ui/CopyableField.java @@ -0,0 +1,78 @@ +package cn.classfun.droidvm.lib.ui; + +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.view.View; +import android.view.ViewParent; +import android.widget.EditText; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.google.android.material.textfield.TextInputLayout; + +import cn.classfun.droidvm.R; + +/** + * Helpers for the read-only boot-info fields (kernel / initrd / cmdline / + * source). They show values the user may want to copy but not type into, so + * each becomes selectable (long-press → Copy) and its enclosing + * {@link TextInputLayout} gets a copy end-icon. + */ +public final class CopyableField { + private CopyableField() { + } + + /** + * Read-only but selectable (long-press Copy works), plus a copy end-icon. + * {@code setTextIsSelectable} alone does not stop an EditText from + * being typed into — only dropping the key listener does — so we do both + * and suppress the soft keyboard / caret. + */ + public static void setupReadOnly(@NonNull EditText et, @NonNull CharSequence label) { + makeReadOnly(et); + addCopyIcon(et, label); + } + + /** Make an EditText non-editable yet still selectable and copyable. */ + public static void makeReadOnly(@NonNull EditText et) { + et.setKeyListener(null); // no text input -> not editable + et.setTextIsSelectable(true); // long-press select + Copy + et.setCursorVisible(false); + et.setShowSoftInputOnFocus(false); // tapping never raises the keyboard + } + + /** + * Just the copy end-icon — for a field that must stay editable to capture + * input (e.g. one that redirects edits elsewhere) yet still offers copy. + */ + public static void addCopyIcon(@NonNull EditText et, @NonNull CharSequence label) { + var til = enclosingLayout(et); + if (til == null) return; + til.setEndIconMode(TextInputLayout.END_ICON_CUSTOM); + til.setEndIconDrawable(R.drawable.ic_content_copy); + til.setEndIconContentDescription(et.getContext().getString(R.string.field_copy)); + til.setEndIconOnClickListener(v -> { + var t = et.getText(); + copy(et.getContext(), t == null ? "" : t.toString(), label); + }); + } + + /** Puts {@code text} on the clipboard and confirms with a short toast. */ + public static void copy( + @NonNull Context ctx, @NonNull CharSequence text, @NonNull CharSequence label) { + var cm = ctx.getSystemService(ClipboardManager.class); + if (cm == null) return; + cm.setPrimaryClip(ClipData.newPlainText(label, text)); + Toast.makeText(ctx, R.string.field_copied, Toast.LENGTH_SHORT).show(); + } + + @Nullable + private static TextInputLayout enclosingLayout(@NonNull View v) { + ViewParent p = v.getParent(); + while (p != null && !(p instanceof TextInputLayout)) p = p.getParent(); + return (TextInputLayout) p; + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/ui/disk/operation/DiskOperationActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/disk/operation/DiskOperationActivity.java index 12f09fa9d..34f306370 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/disk/operation/DiskOperationActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/disk/operation/DiskOperationActivity.java @@ -46,6 +46,11 @@ public final class DiskOperationActivity extends AppCompatActivity { private static final String TAG = "DiskOperationActivity"; public static final String EXTRA_DISK_ID = "disk_id"; public static final String EXTRA_TASK_JSON = "task_json"; + /** Path-mode (no registered DiskConfig): operate on this file directly. */ + public static final String EXTRA_DISK_PATH = "disk_path"; + public static final String EXTRA_DISK_NAME = "disk_name"; + /** On success, {@code setResult(RESULT_OK)} and finish so a launcher can chain. */ + public static final String EXTRA_AUTOFINISH = "autofinish"; private final Handler mainHandler = new Handler(Looper.getMainLooper()); private TerminalView terminalView; private ProgressBar progressSpinner; @@ -56,6 +61,7 @@ public final class DiskOperationActivity extends AppCompatActivity { private MaterialToolbar toolbar; private TerminalSession session; private boolean finished = false; + private boolean autoFinish = false; private String outputPath = null; private String taskAction = null; private DiskStore diskStore = null; @@ -91,6 +97,34 @@ public static Intent createIntent( return intent; } + /** + * Intent that optimizes (and thereby decompresses) the qcow2 at + * {@code path} in place and returns {@code RESULT_OK} on success — for + * launching via an {@code ActivityResultLauncher} so the caller can chain + * (e.g. start the VM once a crosvm-unreadable compressed image is fixed). + * Works without a registered {@link DiskConfig}, since a VM disk may point + * at an unregistered file. + */ + @NonNull + public static Intent optimizeForResultIntent( + @NonNull Context context, + @NonNull String path, + @NonNull String name + ) { + var intent = new Intent(context, DiskOperationActivity.class); + try { + var obj = new JSONObject(); + obj.put("action", "convert"); // no compress -> rewrites uncompressed + intent.putExtra(EXTRA_TASK_JSON, obj.toString()); + } catch (Exception e) { + Log.e(TAG, "Failed to build optimize task", e); + } + intent.putExtra(EXTRA_DISK_PATH, path); + intent.putExtra(EXTRA_DISK_NAME, name); + intent.putExtra(EXTRA_AUTOFINISH, true); + return intent; + } + public static void startOptimize( @NonNull Context context, @NonNull UUID diskId @@ -150,22 +184,37 @@ public void handleOnBackPressed() { var intent = getIntent(); var diskIdStr = intent.getStringExtra(EXTRA_DISK_ID); var taskJsonStr = intent.getStringExtra(EXTRA_TASK_JSON); - if (diskIdStr == null || taskJsonStr == null) { - Log.e(TAG, "Missing intent extras"); + autoFinish = intent.getBooleanExtra(EXTRA_AUTOFINISH, false); + if (taskJsonStr == null) { + Log.e(TAG, "Missing task JSON"); finish(); return; } - var diskId = UUID.fromString(diskIdStr); diskStore = new DiskStore(); diskStore.load(this); - diskConfig = diskStore.findById(diskId); - if (diskConfig == null) { - Log.e(TAG, fmt("Disk not found: %s", diskId)); - finish(); - return; + final String diskPath; + final String diskName; + if (diskIdStr != null) { + diskConfig = diskStore.findById(UUID.fromString(diskIdStr)); + if (diskConfig == null) { + Log.e(TAG, fmt("Disk not found: %s", diskIdStr)); + finish(); + return; + } + diskPath = diskConfig.getFullPath(); + diskName = diskConfig.getName(); + } else { + // Path mode: operate on the file directly (no DiskStore entry). + diskPath = intent.getStringExtra(EXTRA_DISK_PATH); + if (diskPath == null) { + Log.e(TAG, "Missing disk id and path"); + finish(); + return; + } + var name = intent.getStringExtra(EXTRA_DISK_NAME); + diskName = name != null ? name : basename(diskPath); } - var diskPath = diskConfig.getFullPath(); - tvFilename.setText(diskConfig.getName()); + tvFilename.setText(diskName); tvStatus.setText(R.string.disk_operation_running); runOnPool(() -> { final String cmd; @@ -204,7 +253,9 @@ private void onProcessFinished() { if (finished) return; finished = true; int exitCode = session == null ? -1 : session.getExitStatus(); - if (exitCode == 0 && outputPath != null) { + // Path mode (no registered DiskConfig) is an in-place op, so there is + // nothing to persist — skip the store update. + if (exitCode == 0 && outputPath != null && diskConfig != null) { if (taskAction.equals("clone")) { var cloned = new DiskConfig(); if (outputPath.contains("/")) { @@ -225,6 +276,14 @@ private void onProcessFinished() { } diskStore.save(this); } + // Chained convert (e.g. pre-start decompress): hand control back to the + // launcher, which starts the VM. No success screen — the start is the + // feedback. + if (exitCode == 0 && autoFinish) { + setResult(RESULT_OK); + finish(); + return; + } progressSpinner.setVisibility(GONE); ivStatus.setVisibility(VISIBLE); btnCancel.setVisibility(GONE); diff --git a/app/src/main/java/cn/classfun/droidvm/ui/main/vm/MainVMFragment.java b/app/src/main/java/cn/classfun/droidvm/ui/main/vm/MainVMFragment.java index 68e5f92d3..8a6eed454 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/main/vm/MainVMFragment.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/main/vm/MainVMFragment.java @@ -8,8 +8,11 @@ import android.view.MenuItem; import android.view.View; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.MenuRes; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.google.android.material.dialog.MaterialAlertDialogBuilder; @@ -34,6 +37,20 @@ public final class MainVMFragment extends MainStatefulFragment implements ForegroundCallback { private final AtomicBoolean wantOpenConsole = new AtomicBoolean(false); + // Pre-start convert (decompress a crosvm-unreadable qcow2): run this once + // the convert Activity returns RESULT_OK. + @Nullable + private Runnable pendingAfterConvert; + private final ActivityResultLauncher convertResultLauncher = + registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { + var cb = pendingAfterConvert; + pendingAfterConvert = null; + if (result.getResultCode() == Activity.RESULT_OK && cb != null) cb.run(); + }); + private final VMActions.ConvertLauncher convertLauncher = (intent, onConverted) -> { + pendingAfterConvert = onConverted; + convertResultLauncher.launch(intent); + }; public MainVMFragment() { super(VMAdapter.class); @@ -128,7 +145,7 @@ public void onVMStateChanged(UUID vmId, VMState state) { @Override protected void onActionClicked(VMConfig config, VMState currentState) { if (currentState == VMState.STOPPED) { - VMActions.createAndStart(config, mainHandler, uiContext, wantOpenConsole); + VMActions.createAndStart(config, mainHandler, uiContext, wantOpenConsole, convertLauncher); } else if (currentState == VMState.RUNNING) { VMActions.sendCommand("vm_stop", config.getId(), mainHandler, uiContext); } diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/VMActions.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/VMActions.java index 7c7d5d0b0..2132aa59d 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/VMActions.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/VMActions.java @@ -3,26 +3,41 @@ import static android.widget.Toast.LENGTH_LONG; import static cn.classfun.droidvm.lib.Constants.PATH_BUILTIN_INITRD; import static cn.classfun.droidvm.lib.Constants.PATH_BUILTIN_KERNEL; +import static cn.classfun.droidvm.lib.store.enums.Enums.optEnum; +import static cn.classfun.droidvm.lib.utils.StringUtils.basename; import static cn.classfun.droidvm.ui.main.settings.MainSettingsFragment.isAutoConsoleEnabled; import static cn.classfun.droidvm.ui.main.settings.MainSettingsFragment.isClearLogsBeforeStartEnabled; import android.content.Context; +import android.content.Intent; +import android.os.CountDownTimer; import android.os.Handler; import android.util.Log; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +import org.json.JSONArray; + +import java.util.ArrayList; +import java.util.List; import java.util.UUID; import java.util.concurrent.atomic.AtomicBoolean; import cn.classfun.droidvm.R; import cn.classfun.droidvm.lib.daemon.DaemonConnection; +import cn.classfun.droidvm.lib.store.base.DataItem; import cn.classfun.droidvm.lib.store.vm.BootConfig; +import cn.classfun.droidvm.lib.store.vm.VMBackend; import cn.classfun.droidvm.lib.store.vm.VMConfig; import cn.classfun.droidvm.lib.store.vm.VMStore; import cn.classfun.droidvm.lib.ui.UIContext; +import cn.classfun.droidvm.ui.disk.create.DiskFormat; +import cn.classfun.droidvm.ui.disk.operation.DiskOperationActivity; import cn.classfun.droidvm.ui.vm.boot.BootMenuDialog; public final class VMActions { @@ -31,7 +46,31 @@ public final class VMActions { private VMActions() { } + /** + * Launches a {@link DiskOperationActivity} convert and runs + * {@code onConverted} when it returns {@code RESULT_OK}. The call site + * owns an {@code ActivityResultLauncher}; this lets the static start flow + * chain a pre-start disk convert without holding an Activity itself. + */ + public interface ConvertLauncher { + void launch(@NonNull Intent intent, @NonNull Runnable onConverted); + } + public static void createAndStart( + @NonNull VMConfig config, + @NonNull Handler mainHandler, + @NonNull UIContext ui, + @NonNull AtomicBoolean wantOpenConsole, + @Nullable ConvertLauncher convertLauncher + ) { + // crosvm can't read compressed qcow2 (the guest gets vda I/O errors + // and an unreadable partition table, so any root= hangs). Catch it + // before start and offer to convert; everything else starts normally. + guardCompressedDisks(config, mainHandler, ui, convertLauncher, + () -> startAfterGuard(config, mainHandler, ui, wantOpenConsole)); + } + + private static void startAfterGuard( @NonNull VMConfig config, @NonNull Handler mainHandler, @NonNull UIContext ui, @@ -56,6 +95,136 @@ public static void createAndStart( doCreateAndStart(config, mainHandler, ui, wantOpenConsole, null); } + /** + * Pre-start check: for a crosvm VM with qcow2 disks, ask the daemon + * (lbx) which are zlib-compressed and therefore unreadable by crosvm. If + * any are, prompt to convert (decompress) them, then {@code proceed}; + * otherwise {@code proceed} straight away. A check failure never blocks a + * start — a real boot would surface the problem. Callbacks arrive on the + * daemon thread, so UI work is posted to {@code mainHandler}. + */ + private static void guardCompressedDisks( + @NonNull VMConfig config, + @NonNull Handler mainHandler, + @NonNull UIContext ui, + @Nullable ConvertLauncher convertLauncher, + @NonNull Runnable proceed + ) { + if (convertLauncher == null + || optEnum(config.item, "backend", VMBackend.DEFAULT) != VMBackend.CROSVM) { + proceed.run(); + return; + } + var qcow2 = qcow2DiskPaths(config); + if (qcow2.isEmpty()) { + proceed.run(); + return; + } + var images = new JSONArray(); + for (var p : qcow2) images.put(p); + DaemonConnection.getInstance().buildRequest("disk_compat") + .put("images", images) + .onResponse(resp -> { + var compressed = resp.optJSONArray("compressed"); + if (compressed == null || compressed.length() == 0) + mainHandler.post(proceed); + else + mainHandler.post(() -> + promptConvert(config, ui, convertLauncher, compressed, proceed)); + }) + .onUnsuccessful(resp -> mainHandler.post(proceed)) + .onError(e -> { + Log.w(TAG, "disk_compat check failed", e); + mainHandler.post(proceed); + }) + .invoke(); + } + + private static void promptConvert( + @NonNull VMConfig config, + @NonNull UIContext ui, + @NonNull ConvertLauncher convertLauncher, + @NonNull JSONArray compressed, + @NonNull Runnable proceed + ) { + if (!ui.isAlive()) return; + var ctx = ui.getContext(); + var files = new StringBuilder(); + for (int i = 0; i < compressed.length(); i++) { + var p = compressed.optString(i, ""); + if (!p.isEmpty()) files.append("\n• ").append(basename(p)); + } + var dialog = new MaterialAlertDialogBuilder(ctx) + .setTitle(R.string.vm_compressed_disk_title) + .setMessage(ctx.getString(R.string.vm_compressed_disk_message, files.toString())) + .setPositiveButton(R.string.vm_compressed_disk_convert, + (d, w) -> convertNext(ctx, convertLauncher, compressed, 0, proceed)) + .setNeutralButton(R.string.vm_compressed_disk_start_anyway, (d, w) -> proceed.run()) + .setNegativeButton(android.R.string.cancel, null) + .create(); + dialog.show(); + // No response in 5s = "start anyway" (the chosen default), so an + // unattended start isn't blocked; the countdown shows on that button. + var startAnyway = dialog.getButton(AlertDialog.BUTTON_NEUTRAL); + if (startAnyway != null) + startAnyway.setText(ctx.getString(R.string.vm_compressed_disk_start_countdown, 5)); + var timer = new CountDownTimer(5000, 1000) { + @Override + public void onTick(long ms) { + if (startAnyway != null) + startAnyway.setText(ctx.getString( + R.string.vm_compressed_disk_start_countdown, + (int) Math.ceil(ms / 1000.0))); + } + + @Override + public void onFinish() { + dialog.dismiss(); + proceed.run(); + } + }; + // Any interaction (a button tap dismisses the dialog) stops the countdown. + dialog.setOnDismissListener(d -> timer.cancel()); + timer.start(); + } + + /** Convert each compressed disk in turn, then {@code proceed} to start. */ + private static void convertNext( + @NonNull Context ctx, + @NonNull ConvertLauncher convertLauncher, + @NonNull JSONArray compressed, + int i, + @NonNull Runnable proceed + ) { + if (i >= compressed.length()) { + proceed.run(); + return; + } + var path = compressed.optString(i, ""); + if (path.isEmpty()) { + convertNext(ctx, convertLauncher, compressed, i + 1, proceed); + return; + } + var intent = DiskOperationActivity.optimizeForResultIntent(ctx, path, basename(path)); + convertLauncher.launch(intent, + () -> convertNext(ctx, convertLauncher, compressed, i + 1, proceed)); + } + + /** Absolute paths of the VM's qcow2 disks (the ones crosvm reads as blocks). */ + @NonNull + private static List qcow2DiskPaths(@NonNull VMConfig config) { + var out = new ArrayList(); + var disks = config.item.opt("disks", null); + if (disks != null && disks.is(DataItem.Type.ARRAY)) { + for (var disk : disks.asArray()) { + var path = disk.optString("path", ""); + if (!path.isEmpty() && DiskFormat.fromFilename(path) == DiskFormat.QCOW2) + out.add(path); + } + } + return out; + } + /** * Persists a "remember this choice" boot-menu selection to the on-disk * VM store — the source of truth the list reloads on resume, which is diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/boot/BootEntries.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/boot/BootEntries.java index 85066a8d5..bcd781cc4 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/boot/BootEntries.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/boot/BootEntries.java @@ -196,8 +196,13 @@ private static void runAnalyze( ) { Process process = null; try { + // --quick: this URL preview only needs the entry list + cmdline + + // dma warning, so skip the per-entry kernel size/compression reads + // (extra range fetches a listing never uses). The local scan + // (vm_bootscan) stays on the full command for BootPlan. process = new ProcessBuilder( - lbx, "--timeout", String.valueOf(timeoutSecs), "entries", url, "--json" + lbx, "--timeout", String.valueOf(timeoutSecs), + "entries", url, "--json", "--quick" ).start(); var proc = process; // Drain stdout (the JSON) on its own thread so the pipe never diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/boot/VMEditBootTab.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/boot/VMEditBootTab.java index 75d6f7324..77dfded91 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/boot/VMEditBootTab.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/boot/VMEditBootTab.java @@ -13,11 +13,13 @@ import static cn.classfun.droidvm.lib.utils.FileUtils.shellAsyncCheckExists; import static cn.classfun.droidvm.lib.utils.StringUtils.resolveUriPath; +import android.content.Context; import android.text.Spannable; import android.text.SpannableString; import android.text.style.ForegroundColorSpan; import android.view.MenuItem; import android.view.View; +import android.view.inputmethod.InputMethodManager; import android.widget.EditText; import android.widget.ImageButton; import android.widget.TextView; @@ -38,6 +40,7 @@ import cn.classfun.droidvm.lib.store.vm.ProtectedVM; import cn.classfun.droidvm.lib.store.vm.VMConfig; import cn.classfun.droidvm.lib.store.vm.VMStore; +import cn.classfun.droidvm.lib.ui.CopyableField; import cn.classfun.droidvm.lib.ui.IconItemAdapter; import cn.classfun.droidvm.lib.ui.MenuDialogBuilder; import cn.classfun.droidvm.ui.vm.boot.BootEntries; @@ -121,6 +124,15 @@ public void initView() { inputKernel = view.findViewById(R.id.input_kernel); inputInitrd = view.findViewById(R.id.input_initrd); inputCmdline = view.findViewById(R.id.input_cmdline); + // Detection fields are read-only: selectable (long-press Copy) + a copy + // end-icon, never editable. Tapping the cmdline (intent to edit it) + // seeds the override field below and jumps the caret there. + CopyableField.setupReadOnly(etDetectKernel, parent.getString(R.string.edit_vm_boot_detect_kernel)); + CopyableField.setupReadOnly(etDetectInitrd, parent.getString(R.string.edit_vm_boot_detect_initrd)); + CopyableField.setupReadOnly(etDetectSource, parent.getString(R.string.edit_vm_boot_detect_source)); + CopyableField.setupReadOnly(etDetectCmdline, parent.getString(R.string.edit_vm_boot_detect_cmdline)); + etDetectCmdline.setOnClickListener( + v -> redirectCmdlineEditToOverride(etDetectCmdline.getSelectionStart())); } @Override @@ -424,6 +436,27 @@ private void setDetectFields( etDetectSource.setText(source); } + /** + * The detected cmdline is read-only; tapping it (intent to edit) seeds the + * override field with the same text and moves the caret to the tapped + * offset, so customizing continues seamlessly in the editable override. + * Long-press still selects/copies (it doesn't fire the click). + */ + private void redirectCmdlineEditToOverride(int caret) { + var t = etDetectCmdline.getText(); + var detected = t == null ? "" : t.toString(); + // Seed the override from the preview only when it has no edits yet, so + // re-tapping the preview never clobbers an existing customization. + var ov = etCmdlineOverride.getText(); + if (ov == null || ov.length() == 0) + etCmdlineOverride.setText(detected); + etCmdlineOverride.requestFocus(); + etCmdlineOverride.setSelection(Math.max(0, Math.min(caret, etCmdlineOverride.length()))); + var imm = (InputMethodManager) parent.getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null) + imm.showSoftInput(etCmdlineOverride, InputMethodManager.SHOW_IMPLICIT); + } + private void updateDetectionCard() { var entries = scanned; if (entries == null) return; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/info/VMInfoActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/info/VMInfoActivity.java index 0bc38a88e..bfa1be85a 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/info/VMInfoActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/info/VMInfoActivity.java @@ -21,6 +21,8 @@ import android.widget.TextView; import android.widget.Toast; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; @@ -63,6 +65,20 @@ public final class VMInfoActivity extends AppCompatActivity implements Foregroun private final UIContext ui = UIContext.fromActivity(this); private final Handler mainHandler = new Handler(Looper.getMainLooper()); private final AtomicBoolean wantOpenConsole = new AtomicBoolean(false); + // Pre-start convert (decompress a crosvm-unreadable qcow2): run this once + // the convert Activity returns RESULT_OK. + @Nullable + private Runnable pendingAfterConvert; + private final ActivityResultLauncher convertResultLauncher = + registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { + var cb = pendingAfterConvert; + pendingAfterConvert = null; + if (result.getResultCode() == RESULT_OK && cb != null) cb.run(); + }); + private final VMActions.ConvertLauncher convertLauncher = (intent, onConverted) -> { + pendingAfterConvert = onConverted; + convertResultLauncher.launch(intent); + }; private final ConsoleButton toolConsole = new ConsoleButton(this); private VMState oldState = VMState.STOPPED; private CollapsingToolbarLayout collapsingToolbar; @@ -410,7 +426,7 @@ private void onSuspendResumeClicked() { private void doStart() { if (config == null) return; - VMActions.createAndStart(config, mainHandler, ui, wantOpenConsole); + VMActions.createAndStart(config, mainHandler, ui, wantOpenConsole, convertLauncher); } private void doStop() { diff --git a/app/src/main/java/cn/classfun/droidvm/ui/widgets/tools/KernelAnalysisWidget.java b/app/src/main/java/cn/classfun/droidvm/ui/widgets/tools/KernelAnalysisWidget.java index 93b261411..97c63249c 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/widgets/tools/KernelAnalysisWidget.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/widgets/tools/KernelAnalysisWidget.java @@ -23,6 +23,7 @@ import java.util.ArrayList; import cn.classfun.droidvm.R; +import cn.classfun.droidvm.lib.ui.CopyableField; import cn.classfun.droidvm.lib.ui.IconItemAdapter; import cn.classfun.droidvm.ui.vm.boot.BootEntries; import cn.classfun.droidvm.ui.widgets.row.DropdownRowWidget; @@ -89,6 +90,12 @@ private void init() { etKernel = findViewById(R.id.et_analysis_kernel); etInitrd = findViewById(R.id.et_analysis_initrd); etCmdline = findViewById(R.id.et_analysis_cmdline); + // Cloud-analysis fields are read-only: selectable (long-press Copy) with + // a copy end-icon, never editable. + var ctx = getContext(); + CopyableField.setupReadOnly(etKernel, ctx.getString(R.string.edit_vm_boot_detect_kernel)); + CopyableField.setupReadOnly(etInitrd, ctx.getString(R.string.edit_vm_boot_detect_initrd)); + CopyableField.setupReadOnly(etCmdline, ctx.getString(R.string.edit_vm_boot_detect_cmdline)); btnAnalyze.setOnClickListener(v -> analyze()); } diff --git a/app/src/main/res/drawable/ic_content_copy.xml b/app/src/main/res/drawable/ic_content_copy.xml new file mode 100644 index 000000000..4227c21ec --- /dev/null +++ b/app/src/main/res/drawable/ic_content_copy.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/partial_vm_edit_boot.xml b/app/src/main/res/layout/partial_vm_edit_boot.xml index cf6bad958..47a7f6bef 100644 --- a/app/src/main/res/layout/partial_vm_edit_boot.xml +++ b/app/src/main/res/layout/partial_vm_edit_boot.xml @@ -139,7 +139,6 @@ android:id="@+id/et_detect_kernel" android:layout_width="match_parent" android:layout_height="wrap_content" - android:enabled="false" android:inputType="text" android:maxLines="1" android:textSize="14sp" @@ -168,7 +167,6 @@ android:id="@+id/et_detect_initrd" android:layout_width="match_parent" android:layout_height="wrap_content" - android:enabled="false" android:inputType="text" android:maxLines="1" android:textSize="14sp" @@ -186,7 +184,6 @@ android:id="@+id/et_detect_cmdline" android:layout_width="match_parent" android:layout_height="wrap_content" - android:enabled="false" android:inputType="textMultiLine" android:textSize="14sp" tools:ignore="TextContrastCheck,VisualLintTextFieldSize" /> @@ -225,7 +222,7 @@ android:id="@+id/et_cmdline_override" android:layout_width="match_parent" android:layout_height="wrap_content" - android:inputType="text" + android:inputType="textMultiLine" android:textSize="14sp" tools:ignore="TextContrastCheck,VisualLintTextFieldSize" /> @@ -241,7 +238,6 @@ android:id="@+id/et_detect_source" android:layout_width="match_parent" android:layout_height="wrap_content" - android:enabled="false" android:inputType="text" android:maxLines="1" android:textSize="14sp" diff --git a/app/src/main/res/layout/view_kernel_analysis.xml b/app/src/main/res/layout/view_kernel_analysis.xml index f87f6c20b..3977bbe1f 100644 --- a/app/src/main/res/layout/view_kernel_analysis.xml +++ b/app/src/main/res/layout/view_kernel_analysis.xml @@ -92,7 +92,6 @@ android:id="@+id/et_analysis_kernel" android:layout_width="match_parent" android:layout_height="wrap_content" - android:enabled="false" android:inputType="text" android:maxLines="1" android:textSize="14sp" @@ -121,7 +120,6 @@ android:id="@+id/et_analysis_initrd" android:layout_width="match_parent" android:layout_height="wrap_content" - android:enabled="false" android:inputType="text" android:maxLines="1" android:textSize="14sp" @@ -139,7 +137,6 @@ android:id="@+id/et_analysis_cmdline" android:layout_width="match_parent" android:layout_height="wrap_content" - android:enabled="false" android:inputType="textMultiLine" android:textSize="14sp" tools:ignore="TextContrastCheck,VisualLintTextFieldSize" /> diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index febfca891..07d00a40f 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -224,7 +224,7 @@ 移除网络 MAC 地址(可选) 随机 - 源 MAC 過濾 + 源 MAC 过滤 端口隔离 VLAN ID VLAN @@ -363,6 +363,13 @@ 链接 与守护进程通信失败。 + 压缩的磁盘镜像 + crosvm 无法读取压缩的 qcow2 镜像——启动时会出现磁盘 I/O 错误且找不到分区表。要先转换成非压缩再启动吗?\n%1$s + 转换并启动 + 仍要启动 + 仍要启动 (%1$d) + 复制 + 已复制到剪贴板 磁盘图标 空间 %1$s \u00B7 实际 %2$s \u00B7 %3$s @@ -622,8 +629,8 @@ 已暂停,等待 Wi-Fi… 连接中断,重试中 (%1$d/%2$d)… 已有下载正在进行,请等待完成。 - 下载将在后台继续,可在通知栏查看。 无法开始下载,请确认系统下载管理器已启用。 - + 下载将在后台继续,可在通知栏查看。 + 无法开始下载,请确认系统下载管理器已启用。 下载 磁盘镜像下载通知 导入完成 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5e8280518..7fa44eef9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -399,6 +399,13 @@ F12 Failed to communicate with daemon. + Compressed disk image + crosvm can\'t read compressed qcow2 images — the VM would boot to disk I/O errors with no partition table. Convert it to uncompressed and start?\n%1$s + Convert & start + Start anyway + Start anyway (%1$d) + Copy + Copied to clipboard Disk icon %1$s space \u00B7 %2$s on disk \u00B7 %3$s From 102d74ee0a5ffbec5f1a3a2cffde1152ad994440 Mon Sep 17 00:00:00 2001 From: HuJK Date: Mon, 15 Jun 2026 10:18:49 +0800 Subject: [PATCH 015/182] fix(source code)remove non-ascii code --- app/build.gradle.kts | 4 +-- .../daemon/network/backend/BridgeDhcp.java | 8 +++--- .../network/backend/DefaultRouterWatcher.java | 6 ++--- .../network/backend/FirewallHelper.java | 2 +- .../network/backend/LinuxBridgeBackend.java | 18 ++++++------- .../daemon/network/backend/LinuxNetwork.java | 2 +- .../network/backend/ManagedProcess.java | 2 +- .../daemon/network/backend/Netbox.java | 2 +- .../network/backend/UplinkResolver.java | 4 +-- .../backend/gvisor/GvisorBridgeBackend.java | 2 +- .../classfun/droidvm/daemon/vm/BootPlan.java | 8 +++--- .../lib/download/DiskDownloadManager.java | 6 ++--- .../lib/download/DiskDownloadService.java | 6 ++--- .../lib/store/network/NetworkConfig.java | 2 +- .../droidvm/lib/store/vm/BootConfig.java | 6 ++--- .../droidvm/lib/store/vm/VMConfig.java | 2 +- .../droidvm/lib/store/vm/VMNicConfig.java | 6 ++--- .../droidvm/lib/ui/CopyableField.java | 6 ++--- .../ui/disk/lxc/ImportLxcImagesActivity.java | 2 +- .../disk/operation/DiskOperationActivity.java | 6 ++--- .../ui/network/info/NetworkInfoActivity.java | 4 +-- .../cn/classfun/droidvm/ui/vm/VMActions.java | 8 +++--- .../droidvm/ui/vm/boot/BootEntries.java | 4 +-- .../droidvm/ui/vm/boot/BootMenuDialog.java | 6 ++--- .../ui/vm/edit/boot/VMEditBootTab.java | 6 ++--- .../ui/vm/edit/network/VMNetEditAdapter.java | 2 +- .../droidvm/ui/vm/info/VMInfoActivity.java | 2 +- .../ui/widgets/tools/DownloadWidget.java | 4 +-- .../widgets/tools/KernelAnalysisWidget.java | 2 +- .../main/res/layout/view_kernel_analysis.xml | 2 +- app/src/main/res/values/strings.xml | 26 +++++++++---------- app/src/main/rust/netbox/Cargo.toml | 2 +- app/src/main/rust/netbox/build.sh | 2 +- app/src/main/rust/netbox/src/main.rs | 10 +++---- app/src/main/rust/netbox/src/nl.rs | 10 +++---- app/src/main/rust/netbox/src/tap.rs | 6 ++--- .../lib/store/network/VlanConfigPdTest.java | 2 +- 37 files changed, 99 insertions(+), 99 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index da565c190..2c6824937 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -99,7 +99,7 @@ android { jniLibs { // Extract native libs to a real on-disk dir so lbx (shipped as // liblbx.so) is an executable file the app can run from its own - // nativeLibraryDir — no root, no daemon needed for a URL fetch. + // nativeLibraryDir -- no root, no daemon needed for a URL fetch. useLegacyPackaging = true } } @@ -140,7 +140,7 @@ abstract class CopyNativeBinAssetsTask : DefaultTask() { } // Ship lbx as `liblbx.so` in jniLibs so the (non-root) UI process can exec it -// from its nativeLibraryDir — the data-dir copy used by the daemon isn't +// from its nativeLibraryDir -- the data-dir copy used by the daemon isn't // executable by the app's untrusted_app SELinux domain. abstract class CopyLbxJniLibsTask : DefaultTask() { @get:InputDirectory diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/BridgeDhcp.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/BridgeDhcp.java index 44d455611..6aa66ecfe 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/BridgeDhcp.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/BridgeDhcp.java @@ -240,7 +240,7 @@ public JSONArray getPdStatus() { // the raw delegation P/L (context) ... .put("prefix", iface.optString("pd_prefix", "")) // ... and the composed host address P::1/L actually bound - // on the bridge — what the UI shows and what the address + // on the bridge -- what the UI shows and what the address // union deduplicates against. .put("address", iface.optString("pd_address", ""))); } catch (Exception e) { @@ -304,7 +304,7 @@ private JSONObject buildIface(@NonNull VlanConfig vlan) throws Exception { var obj = new JSONObject(); obj.put("name", LinuxNetwork.vlanDevice(br, vlan.getVlanId())); obj.put("tag", String.valueOf(vlan.getVlanId())); - // Served prefixes are authoritative config — bridgedhcp no longer + // Served prefixes are authoritative config -- bridgedhcp no longer // derives the pools/SLAAC prefix from the bridge's live addresses. // v6 is sent only for a static source; DHCP-PD drives it dynamically. var cidr4 = vlan.getIpv4Cidr(); @@ -352,7 +352,7 @@ private JSONObject buildIface(@NonNull VlanConfig vlan) throws Exception { // CRC32 is an unsigned 32-bit value; write it as-is. Casting // to int makes it negative when bit 31 is set, and bridgedhcp // parses "iaid" into a Go uint32, which rejects a negative - // number — the whole helper then fails to start. + // number -- the whole helper then fails to start. // No prefix_len: bridgedhcp binds the host at the delegation's // own length L (suffix ::1), and gates SLAAC/DHCPv6 on L. obj.put("pd", new JSONObject() @@ -424,7 +424,7 @@ private JSONArray[] buildStatics(@NonNull VlanConfig vlan) throws Exception { * present, else the lowest id from PD_ROUTE_TABLE_BASE upward that no * VLAN of any network (running or stopped) holds, persisted * immediately. Stability matters: the id is bridgedhcp's crash-cleanup - * key — a bridge that comes back under a different id would leave the + * key -- a bridge that comes back under a different id would leave the * previous run's rules behind, and reusing a *live* network's id would * wipe its rules at startup. */ diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/DefaultRouterWatcher.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/DefaultRouterWatcher.java index 4c01ec4bc..955f1fb0f 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/DefaultRouterWatcher.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/DefaultRouterWatcher.java @@ -27,7 +27,7 @@ public final class DefaultRouterWatcher { * reachable IPs for port-forward DNAT scoping: Wi-Fi, cellular, VPN, * ethernet, and hotspot/USB/BT tethering. Passed to netbox, which also * drops bridge devices and pbridge-offload addresses. Cellular names other - * than rmnet_data (ccmni, pdp_ip…) are intentionally not matched — same + * than rmnet_data (ccmni, pdp_ip...) are intentionally not matched -- same * assumption the iptables EXT_IFACES list already makes. */ private static final List HOST_IFACE_PREFIXES = List.of( @@ -85,9 +85,9 @@ private void ruleAdd(boolean ipv6, @NonNull String dev, @NonNull String table) { * (the egress half of guest routing: forwarded packets carry no fwmark * and Android's netd rules end in "from all unreachable", so each * bridge needs an explicit rule into the default network's table). - * Declarative per tick: deletes stale rules — wrong table, duplicates + * Declarative per tick: deletes stale rules -- wrong table, duplicates * (added twice across restarts) and [detached] leftovers from a - * recreated bridge — and adds what is missing. Stale-rule deletion by + * recreated bridge -- and adds what is missing. Stale-rule deletion by * spec may remove either twin first; the next tick converges. */ private void reconcileRules(boolean ipv6, @Nullable String table) { diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/FirewallHelper.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/FirewallHelper.java index b46e54182..8f0e5d280 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/FirewallHelper.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/FirewallHelper.java @@ -41,7 +41,7 @@ public abstract boolean removeForwardBase( /** * Installs one DNAT rule scoped to a specific host IP ({@code -d hostIp}), - * so only traffic actually destined to the phone is redirected — transit + * so only traffic actually destined to the phone is redirected -- transit * traffic (other networks, tethered clients) is left alone. Called once * per current host IP; rules are added/removed incrementally as the host * IP set changes. diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/LinuxBridgeBackend.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/LinuxBridgeBackend.java index 8a38f787e..c9d08dcf2 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/LinuxBridgeBackend.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/LinuxBridgeBackend.java @@ -149,7 +149,7 @@ private void startL3() { // the main bridge. Access ports attach to the per-VLAN bridge, // trunk ports to the main bridge; L3 lives on the per-VLAN bridge. // VLAN 0 is the untagged domain: its L3 lives directly on the main - // bridge (no per-VLAN bridge, no VID-0 leg — that would only catch + // bridge (no per-VLAN bridge, no VID-0 leg -- that would only catch // priority-tagged frames, not untagged traffic). // // These bridges and legs face the VMs, so harden them as a router's @@ -225,7 +225,7 @@ private synchronized void snapshotPdUplinks() { /** * Reloads bridgedhcp when a DHCP-PD VLAN's uplink now resolves to a - * different device than the running config has — most importantly when it + * different device than the running config has -- most importantly when it * was unresolved at start (so PD was skipped) and the interface (e.g. * Wi-Fi) has since come up. A still-unresolved uplink is left alone: a * running PD client already retries on its own. Invoked on host-network @@ -254,7 +254,7 @@ private synchronized void reconcilePdUplinks() { /** * PD event from bridgedhcp: the helper already plumbed the address on * the per-VLAN bridge (and DHCPv6/RA follow it automatically); what is - * left is the host side — firewall accept rules for the delegated + * left is the host side -- firewall accept rules for the delegated * subnet and the policy-routing entries for return traffic. */ private synchronized void onPdAddressChanged(int vlanId, @Nullable IPv6Network address) { @@ -383,7 +383,7 @@ private synchronized void ensureVlanBridge(int vid) { * The bridge a NIC's tap attaches to: a trunk port (no VLAN id / the * 4095 marker) or any non-L3 network joins the main bridge; a tagged * access port joins its VLAN's per-VLAN bridge. VLAN 0 (untagged-only) - * ports don't exist on a Linux bridge — validation rejects them, since + * ports don't exist on a Linux bridge -- validation rejects them, since * without VLAN filtering they could only behave as trunks. */ @NonNull @@ -510,15 +510,15 @@ public JSONArray listAddresses() { } /** - * One row per address with three dimensions — VLAN, bound/unbound, - * configured/auto — over the union, in every mode, of every address the + * One row per address with three dimensions -- VLAN, bound/unbound, + * configured/auto -- over the union, in every mode, of every address the * network carries: the live addresses on each bridge device (the main * bridge = VLAN 0, every per-VLAN bridge = its VID) and every configured * CIDR. A configured address that is also live appears once (configured + * bound); a configured one not yet on a device is configured + unbound; a * live one with no config is auto (link-local, RA, the L2 uplink mirror * pbridge parks on the main bridge, ...). The main bridge is always - * scanned — including L2/none mode, where it is the only device and carries + * scanned -- including L2/none mode, where it is the only device and carries * that mirror; per-VLAN bridges include on-demand ones, not just configured. */ @NonNull @@ -531,7 +531,7 @@ public JSONArray listAddressEntries() { for (var vlan : inst.getVlans()) configured.put(vlan.getVlanId(), configuredCidrs(vlan)); // Every VID whose L3 device may carry addresses, in a stable order: - // VLAN 0 (the main bridge, always — the L2 mirror lands here), the + // VLAN 0 (the main bridge, always -- the L2 mirror lands here), the // configured VLANs in config order, then any on-demand per-VLAN bridge. var vids = new java.util.LinkedHashSet(); vids.add(0); @@ -561,7 +561,7 @@ public JSONArray listAddressEntries() { // L2 pseudo-bridge: pbridge parks each learned guest address on the // uplink tagged with the offload magic (so the Wi-Fi firmware answers // ARP/NS for them). Those tagged addresses are exactly the pseudo- - // bridged guests' IPs — surface them under their own source; the metric + // bridged guests' IPs -- surface them under their own source; the metric // filter drops the host's own (untagged) uplink addresses. if (pbridge != null && resolvedUplink != null) { var guests = net.listAddresses(resolvedUplink, Constants.PBRIDGE_OFFLOAD_MAGIC); diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/LinuxNetwork.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/LinuxNetwork.java index 2cf5d1449..0ae109da8 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/LinuxNetwork.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/LinuxNetwork.java @@ -84,7 +84,7 @@ public static String perVlanBridge(@NonNull String bridge, int vid) { /** * The device carrying a VLAN's L3. VLAN 0 is the untagged domain: its * traffic lives on the main bridge itself (the IP stack has no trunk - * semantics — untagged frames are simply what the bridge device sees), + * semantics -- untagged frames are simply what the bridge device sees), * so no per-VLAN bridge and no 802.1q leg exist for it. An 802.1q * subinterface with VID 0 would receive only priority-tagged frames, * never the untagged traffic it is meant to serve. diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/ManagedProcess.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/ManagedProcess.java index c756d4e2e..9aae3542c 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/ManagedProcess.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/ManagedProcess.java @@ -51,7 +51,7 @@ public void setOnUnexpectedExit(@Nullable Runnable callback) { /** * Receives every output line from the monitor thread (in addition to - * logging) — used for child event streams (e.g. bridgedhcp JSON lines). + * logging) -- used for child event streams (e.g. bridgedhcp JSON lines). */ public void setLineListener(@Nullable LineListener listener) { lineListener = listener; diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/Netbox.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/Netbox.java index 7f68754c3..ba5bc25ac 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/Netbox.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/Netbox.java @@ -22,7 +22,7 @@ * The single entry point for every netlink operation the daemon performs. * *

The daemon used to shell out to the system {@code ip}/{@code bridge} - * (iproute2), which vary per OEM — some are too old to even emit + * (iproute2), which vary per OEM -- some are too old to even emit * {@code IFA_RT_PRIORITY} in {@code -j} JSON, the field pbridge tags its * offload-proxy addresses with. {@code netbox} is our bundled, static Rust tool * that does the same operations straight over rtnetlink with a fixed JSON diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/UplinkResolver.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/UplinkResolver.java index 3557d9909..8a3db6c4b 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/UplinkResolver.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/UplinkResolver.java @@ -18,7 +18,7 @@ /** * Enumerates host L2 uplink candidates and resolves the logical * "WiFi"/"Ethernet"/"Tethering" identifiers to a concrete interface at - * network start: device names drift across reboots (wlan0 ⇄ wlan1), so the + * network start: device names drift across reboots (wlan0 <-> wlan1), so the * identifier is stored and re-resolved live; a literal interface name is * pinned and used as-is. */ @@ -82,7 +82,7 @@ private static List scan() { || new File(entry, "wireless").exists(); boolean physical = new File(entry, "device").exists(); boolean ether = readInt(new File(entry, "type")) == ARPHRD_ETHER; - // leaf = not stacked on another netdev (drops veth/vlan/rmnet_data@…) + // leaf = not stacked on another netdev (drops veth/vlan/rmnet_data@...) boolean leaf = readInt(new File(entry, "ifindex")) == readInt(new File(entry, "iflink")); out.add(new Iface(name, wireless, isUp(entry), ether, diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/gvisor/GvisorBridgeBackend.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/gvisor/GvisorBridgeBackend.java index 87072d579..b309830c9 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/gvisor/GvisorBridgeBackend.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/gvisor/GvisorBridgeBackend.java @@ -225,7 +225,7 @@ private void removeStaticLeases(@NonNull VMNicConfig nic, @NonNull String tapNam /** * Installs this NIC's port forwards one at a time so a single bind - * failure (host port already in use — gvswitch local forwards listen + * failure (host port already in use -- gvswitch local forwards listen * on the host) only drops that one rule. Successfully installed refs * are recorded per tap so detach removes exactly what this NIC added; * failures are surfaced via network_info. diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/BootPlan.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/BootPlan.java index 3b6c118ae..ed161591e 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/BootPlan.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/BootPlan.java @@ -37,7 +37,7 @@ * reads) and then a content MD5 of each source file, so an in-place * kernel rebuild that kept the same path and size is still caught. The * MD5 reads the source kernel/initrd out of the image, so a cache hit is - * no longer read-free — but it only happens once the metadata check has + * no longer read-free -- but it only happens once the metadata check has * matched, and a hit still writes nothing. * *

A distro {@code vmlinuz} is often a compressed kernel a direct-boot @@ -45,7 +45,7 @@ * {@code lbx entries} reports this per entry as {@code kernel_compression} * (null = already a raw {@code Image}/x86 bzImage); when set we extract and * MD5 the decompressed kernel ({@code lbx cp/md5 --decompress}) so - * the cached file boots directly. lbx stays faithful by default — the flag + * the cached file boots directly. lbx stays faithful by default -- the flag * is only passed for a kernel lbx flagged as compressed, never the initrd. */ public final class BootPlan { @@ -357,8 +357,8 @@ && cacheUpToDate(image, entry, manifest)) { /** * Whether the cache described by {@code manifest} still reflects the - * guest. Cheap identity first — image, URIs and sizes, all from inode - * metadata with no data reads — then a content MD5 of each source file + * guest. Cheap identity first -- image, URIs and sizes, all from inode + * metadata with no data reads -- then a content MD5 of each source file * via {@code lbx md5}. The MD5 is what catches an in-place kernel * rebuild that kept the same path and byte size; it reads the source * kernel/initrd out of the image, so it only runs once the metadata diff --git a/app/src/main/java/cn/classfun/droidvm/lib/download/DiskDownloadManager.java b/app/src/main/java/cn/classfun/droidvm/lib/download/DiskDownloadManager.java index 1bc6dc6c7..f5547a95d 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/download/DiskDownloadManager.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/download/DiskDownloadManager.java @@ -31,7 +31,7 @@ * {@link HttpURLConnection} (following redirects, resuming via HTTP Range, and * retrying on read timeouts / dropped connections) instead of the system * DownloadManager, whose OEM variants either reject app-private destinations or - * time out aggressively without resuming on this CDN — while plain `curl` works. + * time out aggressively without resuming on this CDN -- while plain `curl` works. * *

Because the app holds MANAGE_EXTERNAL_STORAGE we write straight to the * user's folder (a {@code .part} file, renamed on completion). State lives in an @@ -45,7 +45,7 @@ public final class DiskDownloadManager { private static final int CONNECT_TIMEOUT_MS = 30000; private static final int READ_TIMEOUT_MS = 30000; private static final int BUFFER = 65536; - /** Abort + retry if throughput drops below ~0.7 KB/s over this window — i.e. + /** Abort + retry if throughput drops below ~0.7 KB/s over this window -- i.e. * all but stopped (catches a mirror that holds the connection open but sends * almost nothing); genuinely slow-but-moving connections are left alone. */ private static final long STALL_WINDOW_MS = 1500; @@ -237,7 +237,7 @@ static void runDownload(@NonNull Context context, long id) { var parent = dest.getParentFile(); if (parent != null && !parent.exists() && !parent.mkdirs()) throw new IOException(fmt("Cannot create folder: %s", parent.getAbsolutePath())); - // Start fresh — stale .part from a prior failed attempt may be corrupt. + // Start fresh -- stale .part from a prior failed attempt may be corrupt. if (part.exists() && !part.delete()) Log.w(TAG, fmt("Could not clear stale part file: %s", part.getAbsolutePath())); String lastError = null; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/download/DiskDownloadService.java b/app/src/main/java/cn/classfun/droidvm/lib/download/DiskDownloadService.java index 90bedd573..2d92d5947 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/download/DiskDownloadService.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/download/DiskDownloadService.java @@ -34,7 +34,7 @@ /** * Foreground service that runs {@link DiskDownloadManager#runDownload} on a * background thread and renders one notification per download covering every - * state — connecting, downloading (%/size/speed/ETA), retrying, failed, done — + * state -- connecting, downloading (%/size/speed/ETA), retrying, failed, done -- * in the same notification (no separate completion/failure notification). */ public final class DiskDownloadService extends Service { @@ -255,9 +255,9 @@ private String runningText(Progress p, long speed) { ? getString(R.string.download_size_progress, formatSize(p.downloaded), formatSize(p.total)) : getString(R.string.download_size_unknown, formatSize(p.downloaded)); if (speed > 0) { - text += " · " + getString(R.string.download_speed, formatSize(speed)); + text += " | " + getString(R.string.download_speed, formatSize(speed)); if (p.total > 0) - text += " · " + getString(R.string.download_eta, + text += " | " + getString(R.string.download_eta, formatDuration((p.total - p.downloaded) / speed)); } return text; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/network/NetworkConfig.java b/app/src/main/java/cn/classfun/droidvm/lib/store/network/NetworkConfig.java index c83b98e66..eb4eaa5c7 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/network/NetworkConfig.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/network/NetworkConfig.java @@ -89,7 +89,7 @@ public DataItem l3() { } /** - * L2 mode: configured uplink — a literal interface name or a + * L2 mode: configured uplink -- a literal interface name or a * "WiFi"/"Ethernet"/"Tethering" logical identifier resolved at start. */ @Nullable diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/BootConfig.java b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/BootConfig.java index 4d86dab70..6867f0700 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/BootConfig.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/BootConfig.java @@ -11,7 +11,7 @@ import cn.classfun.droidvm.lib.store.enums.StringEnum; /** - * Wrapper over a VM config's "boot" object — the single source of truth + * Wrapper over a VM config's "boot" object -- the single source of truth * for how the guest is brought up: * *

@@ -36,7 +36,7 @@
  * {@code source} below it) only select which one takes effect, so toggling
  * in the UI never loses the other branch's values. Legacy flat keys
  * ({@code use_uefi}/{@code kernel}/{@code initrd}/{@code cmdline}/
- * {@code bios}) are folded into this object once, on config load — see
+ * {@code bios}) are folded into this object once, on config load -- see
  * {@link VMConfig#migrateBoot}.
  */
 public final class BootConfig {
@@ -81,7 +81,7 @@ public int getStringId() {
 
     /**
      * Reserved {@code vm_start} boot_entry / boot-menu selection key for
-     * "boot this disk with DroidVM's built-in kernel" — not a disk image
+     * "boot this disk with DroidVM's built-in kernel" -- not a disk image
      * entry, so it is matched as a sentinel (see {@code BootPlan.resolve})
      * and can never collide with an lbx entry id (which never start '@').
      */
diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMConfig.java b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMConfig.java
index 4aa00cde5..bb59ec128 100644
--- a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMConfig.java
+++ b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMConfig.java
@@ -44,7 +44,7 @@ private void migrateBoot() {
         var boot = BootConfig.of(this);
         var uefi = item.optBoolean("use_uefi", true);
         // legacy QEMU oddity: use_uefi=false + "bios" + no kernel meant
-        // "boot a custom firmware" — that is UEFI protocol in the new model
+        // "boot a custom firmware" -- that is UEFI protocol in the new model
         if (!uefi && item.optString("kernel", "").isEmpty()
             && !item.optString("bios", "").isEmpty())
             uefi = true;
diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMNicConfig.java b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMNicConfig.java
index 1b3592c05..0988768ce 100644
--- a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMNicConfig.java
+++ b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMNicConfig.java
@@ -120,7 +120,7 @@ private List forwards(@NonNull String key) {
 
     /**
      * The VLAN of this NIC the DHCP services apply to: the pinned access
-     * VLAN, or the untagged domain (VLAN 0) when trunked — either the
+     * VLAN, or the untagged domain (VLAN 0) when trunked -- either the
      * implicit trunk (no vlan_id) or the explicit 4095 trunk marker.
      */
     @Nullable
@@ -148,7 +148,7 @@ public void validate(@NonNull NetworkConfig network) {
             if (vlanId < 0 || vlanId > 4095)
                 throw new IllegalArgumentException(fmt("Invalid NIC VLAN id: %d", vlanId));
             // An untagged-only port needs per-port VLAN filtering, which a
-            // Linux bridge on stock GKI cannot do — attaching to the main
+            // Linux bridge on stock GKI cannot do -- attaching to the main
             // bridge would actually be a trunk (flooded tagged frames
             // included). Refuse instead of silently degrading; gvisor's
             // switchcore can enforce untagged-only, so 0 stays valid there.
@@ -200,7 +200,7 @@ public void validate(@NonNull NetworkConfig network) {
      * An in-memory copy of this NIC with every option the target network can't
      * honour stripped out; the saved config is left untouched. A NIC keeps its
      * L3-only settings (DHCP static lease, port forwards) in the stored config
-     * so they survive being moved back to an L3 network — the GUI just hides
+     * so they survive being moved back to an L3 network -- the GUI just hides
      * them elsewhere. On an L2 / non-DHCP network those settings are inert, so
      * for this run we drop them rather than refuse to start. Genuine
      * misconfigurations are left in place for {@link #validate} to reject.
diff --git a/app/src/main/java/cn/classfun/droidvm/lib/ui/CopyableField.java b/app/src/main/java/cn/classfun/droidvm/lib/ui/CopyableField.java
index f05284041..2c7dcb84d 100644
--- a/app/src/main/java/cn/classfun/droidvm/lib/ui/CopyableField.java
+++ b/app/src/main/java/cn/classfun/droidvm/lib/ui/CopyableField.java
@@ -18,7 +18,7 @@
 /**
  * Helpers for the read-only boot-info fields (kernel / initrd / cmdline /
  * source). They show values the user may want to copy but not type into, so
- * each becomes selectable (long-press → Copy) and its enclosing
+ * each becomes selectable (long-press -> Copy) and its enclosing
  * {@link TextInputLayout} gets a copy end-icon.
  */
 public final class CopyableField {
@@ -28,7 +28,7 @@ private CopyableField() {
     /**
      * Read-only but selectable (long-press Copy works), plus a copy end-icon.
      * {@code setTextIsSelectable} alone does not stop an EditText from
-     * being typed into — only dropping the key listener does — so we do both
+     * being typed into -- only dropping the key listener does -- so we do both
      * and suppress the soft keyboard / caret.
      */
     public static void setupReadOnly(@NonNull EditText et, @NonNull CharSequence label) {
@@ -45,7 +45,7 @@ public static void makeReadOnly(@NonNull EditText et) {
     }
 
     /**
-     * Just the copy end-icon — for a field that must stay editable to capture
+     * Just the copy end-icon -- for a field that must stay editable to capture
      * input (e.g. one that redirects edits elsewhere) yet still offers copy.
      */
     public static void addCopyIcon(@NonNull EditText et, @NonNull CharSequence label) {
diff --git a/app/src/main/java/cn/classfun/droidvm/ui/disk/lxc/ImportLxcImagesActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/disk/lxc/ImportLxcImagesActivity.java
index 9df72c2c7..8f87c23ef 100644
--- a/app/src/main/java/cn/classfun/droidvm/ui/disk/lxc/ImportLxcImagesActivity.java
+++ b/app/src/main/java/cn/classfun/droidvm/ui/disk/lxc/ImportLxcImagesActivity.java
@@ -197,7 +197,7 @@ private void captureSession() {
 
     /**
      * Rebuilds the form from the last saved session (replays the
-     * distro→version→variant→build cascade) and re-attaches the progress bar if a
+     * distro->version->variant->build cascade) and re-attaches the progress bar if a
      * download is still running. The session is kept after the download ends too,
      * so reopening restores the last selections (hit Load to refresh). Returns
      * false if there's nothing to restore.
diff --git a/app/src/main/java/cn/classfun/droidvm/ui/disk/operation/DiskOperationActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/disk/operation/DiskOperationActivity.java
index 34f306370..f641f5669 100644
--- a/app/src/main/java/cn/classfun/droidvm/ui/disk/operation/DiskOperationActivity.java
+++ b/app/src/main/java/cn/classfun/droidvm/ui/disk/operation/DiskOperationActivity.java
@@ -99,7 +99,7 @@ public static Intent createIntent(
 
     /**
      * Intent that optimizes (and thereby decompresses) the qcow2 at
-     * {@code path} in place and returns {@code RESULT_OK} on success — for
+     * {@code path} in place and returns {@code RESULT_OK} on success -- for
      * launching via an {@code ActivityResultLauncher} so the caller can chain
      * (e.g. start the VM once a crosvm-unreadable compressed image is fixed).
      * Works without a registered {@link DiskConfig}, since a VM disk may point
@@ -254,7 +254,7 @@ private void onProcessFinished() {
         finished = true;
         int exitCode = session == null ? -1 : session.getExitStatus();
         // Path mode (no registered DiskConfig) is an in-place op, so there is
-        // nothing to persist — skip the store update.
+        // nothing to persist -- skip the store update.
         if (exitCode == 0 && outputPath != null && diskConfig != null) {
             if (taskAction.equals("clone")) {
                 var cloned = new DiskConfig();
@@ -277,7 +277,7 @@ private void onProcessFinished() {
             diskStore.save(this);
         }
         // Chained convert (e.g. pre-start decompress): hand control back to the
-        // launcher, which starts the VM. No success screen — the start is the
+        // launcher, which starts the VM. No success screen -- the start is the
         // feedback.
         if (exitCode == 0 && autoFinish) {
             setResult(RESULT_OK);
diff --git a/app/src/main/java/cn/classfun/droidvm/ui/network/info/NetworkInfoActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/network/info/NetworkInfoActivity.java
index 77ef9616f..00931ab2e 100644
--- a/app/src/main/java/cn/classfun/droidvm/ui/network/info/NetworkInfoActivity.java
+++ b/app/src/main/java/cn/classfun/droidvm/ui/network/info/NetworkInfoActivity.java
@@ -452,7 +452,7 @@ private void addToolRow(@NonNull LinearLayout container, @NonNull String key, bo
         ((ImageView) view.findViewById(R.id.iv_icon)).setImageResource(iconRes);
         ((TextView) view.findViewById(R.id.tv_line1)).setText(label);
         var line2 = (TextView) view.findViewById(R.id.tv_line2);
-        line2.setText(fmt("%s · %s",
+        line2.setText(fmt("%s | %s",
             getString(running ? R.string.network_info_tool_running
                               : R.string.network_info_tool_stopped),
             getString(R.string.network_info_tool_view_log)));
@@ -669,7 +669,7 @@ private void addPdEntry(@NonNull LinearLayout container, @NonNull JSONObject pd)
         var vlan = pd.optInt("vlan", 0);
         var state = pd.optString("state", "");
         // The composed host address P::1/L actually bound on the bridge (empty
-        // while searching) — so this row matches the live nic address and the
+        // while searching) -- so this row matches the live nic address and the
         // "bound" chip reflects the real binding, not the raw delegation.
         var address = pd.optString("address", "");
         var noUplink = "no_uplink".equals(state);
diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/VMActions.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/VMActions.java
index 2132aa59d..e7562f7ac 100644
--- a/app/src/main/java/cn/classfun/droidvm/ui/vm/VMActions.java
+++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/VMActions.java
@@ -100,7 +100,7 @@ private static void startAfterGuard(
      * (lbx) which are zlib-compressed and therefore unreadable by crosvm. If
      * any are, prompt to convert (decompress) them, then {@code proceed};
      * otherwise {@code proceed} straight away. A check failure never blocks a
-     * start — a real boot would surface the problem. Callbacks arrive on the
+     * start -- a real boot would surface the problem. Callbacks arrive on the
      * daemon thread, so UI work is posted to {@code mainHandler}.
      */
     private static void guardCompressedDisks(
@@ -152,7 +152,7 @@ private static void promptConvert(
         var files = new StringBuilder();
         for (int i = 0; i < compressed.length(); i++) {
             var p = compressed.optString(i, "");
-            if (!p.isEmpty()) files.append("\n• ").append(basename(p));
+            if (!p.isEmpty()) files.append("\n- ").append(basename(p));
         }
         var dialog = new MaterialAlertDialogBuilder(ctx)
             .setTitle(R.string.vm_compressed_disk_title)
@@ -227,7 +227,7 @@ private static List qcow2DiskPaths(@NonNull VMConfig config) {
 
     /**
      * Persists a "remember this choice" boot-menu selection to the on-disk
-     * VM store — the source of truth the list reloads on resume, which is
+     * VM store -- the source of truth the list reloads on resume, which is
      * why a one-shot start that only mutated the in-memory config never
      * stuck. The same change is mirrored onto {@code config} so the daemon
      * gets it via vm_modify. The built-in kernel is baked into a plain
@@ -256,7 +256,7 @@ private static String rememberChoice(
                 store.save(context);
             }
         }
-        // built-in now boots as a plain manual config — no one-shot override
+        // built-in now boots as a plain manual config -- no one-shot override
         return builtin ? null : bootEntry;
     }
 
diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/boot/BootEntries.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/boot/BootEntries.java
index bcd781cc4..bfd43487f 100644
--- a/app/src/main/java/cn/classfun/droidvm/ui/vm/boot/BootEntries.java
+++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/boot/BootEntries.java
@@ -170,7 +170,7 @@ public static void scan(@NonNull String image, @NonNull OnScan callback) {
     private static final long ANALYZE_HARD_TIMEOUT_MS = 5 * 60_000;
 
     /**
-     * Analyze a remote (URL) image by running lbx directly from the UI — a URL
+     * Analyze a remote (URL) image by running lbx directly from the UI -- a URL
      * fetch needs no root, and lbx ships as {@code liblbx.so} in the app's
      * nativeLibraryDir, which the app may execute (unlike the daemon's
      * data-dir copy). So we skip the daemon and stream lbx's
@@ -273,7 +273,7 @@ private static BootEntries parse(@Nullable JSONArray arr) {
 
     /**
      * The entry a boot would use right now: pinned entry by exact id,
-     * then exact title, falling back to the bootloader default — the
+     * then exact title, falling back to the bootloader default -- the
      * same cascade as the daemon's BootPlan.
      */
     @Nullable
diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/boot/BootMenuDialog.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/boot/BootMenuDialog.java
index 68bbf8940..164697cc9 100644
--- a/app/src/main/java/cn/classfun/droidvm/ui/vm/boot/BootMenuDialog.java
+++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/boot/BootMenuDialog.java
@@ -250,7 +250,7 @@ private static void updateSummary(
             summary.setVisibility(GONE);
             return;
         }
-        var line = entry.kernel + " · " + entry.source;
+        var line = entry.kernel + " | " + entry.source;
         var cmdline = entry.effectiveCmdline(boot.isVdafix());
         if (!cmdline.isEmpty()) line += "\n" + cmdline;
         summary.setVisibility(VISIBLE);
@@ -320,7 +320,7 @@ private static CharSequence warnedLabel(
 
     /**
      * Whether this VM boots as a gunyah protected VM (protected, or
-     * protected-without-firmware) — read from the stored config, where a
+     * protected-without-firmware) -- read from the stored config, where a
      * guest kernel without CONFIG_DMA_RESTRICTED_POOL cannot drive virtio.
      * Mirrors {@code VMEditBootTab.isProtectedVm}.
      */
@@ -335,7 +335,7 @@ private static boolean isProtectedVm(@NonNull VMConfig config) {
      * Optional warning for a boot entry, or null when it has none. Today the
      * only case is a kernel lbx flagged as lacking CONFIG_DMA_RESTRICTED_POOL
      * on a protected VM (same condition as the edit tab's detection card),
-     * shown here as a terse one-liner since the menu is cramped — the edit
+     * shown here as a terse one-liner since the menu is cramped -- the edit
      * tab keeps the full explanation.
      */
     @Nullable
diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/boot/VMEditBootTab.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/boot/VMEditBootTab.java
index 77dfded91..e1939c240 100644
--- a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/boot/VMEditBootTab.java
+++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/boot/VMEditBootTab.java
@@ -182,7 +182,7 @@ public void onTabShown() {
             rescan();
         } else if (scanned != null) {
             // same image, but the protection mode may have changed in the
-            // basic tab — refresh the detection card so its warning tracks it
+            // basic tab -- refresh the detection card so its warning tracks it
             updateDetectionCard();
         }
     }
@@ -490,7 +490,7 @@ private void updateDetectionCard() {
 
     /**
      * Whether the VM would boot as a gunyah protected VM (protected, or
-     * protected-without-firmware) — read live from the basic tab so a
+     * protected-without-firmware) -- read live from the basic tab so a
      * just-changed selection is reflected, then the stored config, then
      * the backend default. In such a VM a guest kernel without
      * CONFIG_DMA_RESTRICTED_POOL cannot drive virtio.
@@ -547,7 +547,7 @@ private CharSequence highlightRootParams(@NonNull BootEntries.Entry entry, boole
         return span;
     }
 
-    /** The value of {@code key}=… in {@code cmdline}, or null if absent. */
+    /** The value of {@code key}=... in {@code cmdline}, or null if absent. */
     @Nullable
     private static String paramValue(@NonNull String cmdline, @NonNull String key) {
         var m = ROOT_PARAM.matcher(cmdline);
diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/network/VMNetEditAdapter.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/network/VMNetEditAdapter.java
index 7d41a0a0c..231604c81 100644
--- a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/network/VMNetEditAdapter.java
+++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/network/VMNetEditAdapter.java
@@ -260,7 +260,7 @@ private void bindLease(
         fwd.setText(formatForwards(
             v6 ? nic.getDhcp6Forwards() : nic.getDhcp4Forwards()));
         // IPv6 forwards need IPv6 SNAT, which the Linux bridge can't do
-        // (Android kernel has no IPv6 NAT) — hide them there entirely
+        // (Android kernel has no IPv6 NAT) -- hide them there entirely
         boolean fwdSupported = !v6
             || network.getBridgeType() == cn.classfun.droidvm.lib.store.network.BridgeType.GVISOR;
         fwdTil.setVisibility(fwdSupported ? VISIBLE : GONE);
diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/info/VMInfoActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/info/VMInfoActivity.java
index bfa1be85a..c15a9a37a 100644
--- a/app/src/main/java/cn/classfun/droidvm/ui/vm/info/VMInfoActivity.java
+++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/info/VMInfoActivity.java
@@ -231,7 +231,7 @@ private String bootSummary() {
             var pinned = boot.getImageEntry();
             var label = getString(R.string.edit_vm_kernel_source_image);
             if (pinned != null && pinned.title != null)
-                return label + " · " + pinned.title;
+                return label + " | " + pinned.title;
             return label;
         }
         return basename(boot.getKernel());
diff --git a/app/src/main/java/cn/classfun/droidvm/ui/widgets/tools/DownloadWidget.java b/app/src/main/java/cn/classfun/droidvm/ui/widgets/tools/DownloadWidget.java
index e17d7cec4..f5e758d09 100644
--- a/app/src/main/java/cn/classfun/droidvm/ui/widgets/tools/DownloadWidget.java
+++ b/app/src/main/java/cn/classfun/droidvm/ui/widgets/tools/DownloadWidget.java
@@ -20,7 +20,7 @@
 
 /**
  * A progress view for a download owned elsewhere (e.g. {@link android.app.DownloadManager}).
- * It renders only — it never runs a download itself. Drive it from the main thread:
+ * It renders only -- it never runs a download itself. Drive it from the main thread:
  * call {@link #startExternal} once, then feed it {@link #updateExternal} and the
  * {@code markExternal*} state transitions as the download progresses. The cancel
  * button invokes the handler passed to {@link #startExternal}.
@@ -115,7 +115,7 @@ public void updateExternal(long downloaded, long total) {
         updateUI(downloaded, total, speedBytesPerSec);
     }
 
-    /** Renders the download as paused, showing why (waiting for network, retrying, …). */
+    /** Renders the download as paused, showing why (waiting for network, retrying, ...). */
     public void markExternalPaused(@NonNull String reasonText) {
         if (state == STATE_FINISHED) return;
         progressBar.setIndeterminate(false);
diff --git a/app/src/main/java/cn/classfun/droidvm/ui/widgets/tools/KernelAnalysisWidget.java b/app/src/main/java/cn/classfun/droidvm/ui/widgets/tools/KernelAnalysisWidget.java
index 97c63249c..7bcffd49c 100644
--- a/app/src/main/java/cn/classfun/droidvm/ui/widgets/tools/KernelAnalysisWidget.java
+++ b/app/src/main/java/cn/classfun/droidvm/ui/widgets/tools/KernelAnalysisWidget.java
@@ -33,7 +33,7 @@
  * image (a URL); pressing Analyze runs a single cloud analysis
  * ({@code lbx entries  --json}, via the daemon's {@code vm_bootscan})
  * and the card shows, per boot entry, the kernel / initrd / cmdline and the
- * protected-VM DMA warning — the same read-only view the VM edit screen
+ * protected-VM DMA warning -- the same read-only view the VM edit screen
  * gives a local disk image.
  *
  * 

One analysis is one lbx call that returns every entry with every field, diff --git a/app/src/main/res/layout/view_kernel_analysis.xml b/app/src/main/res/layout/view_kernel_analysis.xml index 3977bbe1f..470260497 100644 --- a/app/src/main/res/layout/view_kernel_analysis.xml +++ b/app/src/main/res/layout/view_kernel_analysis.xml @@ -1,7 +1,7 @@ VLAN ID VLAN Untagged-only (0) ports are not supported on a Linux bridge; use a tagged VLAN or trunk - VLAN must be 0–4094, or 4095 for trunk + VLAN must be 0-4094, or 4095 for trunk DHCPv4 static lease DHCPv6 static lease Address offset @@ -281,7 +281,7 @@ UEFI firmware (empty = built-in EDK2) Built-in EDK2 Select UEFI firmware - Custom firmware only takes effect on the QEMU backend\ncrosvm always uses the built-in EDK2\nBooting Linux via UEFI is still being adapted and is not usable yet — to boot Linux, choose the Linux kernel boot protocol + Custom firmware only takes effect on the QEMU backend\ncrosvm always uses the built-in EDK2\nBooting Linux via UEFI is still being adapted and is not usable yet -- to boot Linux, choose the Linux kernel boot protocol Kernel source Disk image Manual @@ -299,7 +299,7 @@ initrd Kernel cmdline Source - Scanning… + Scanning... Scan failed: %1$s No disk image configured for this VM root= names a physical device; enable auto-fix or override the cmdline @@ -311,7 +311,7 @@ Must be a number of seconds Kernel cmdline override (empty = from image) Empty = use cmdline from the image - Select boot entry — %1$s + Select boot entry -- %1$s Boot Boot (%1$d) Remember this choice @@ -400,7 +400,7 @@ Failed to communicate with daemon. Compressed disk image - crosvm can\'t read compressed qcow2 images — the VM would boot to disk I/O errors with no partition table. Convert it to uncompressed and start?\n%1$s + crosvm can\'t read compressed qcow2 images -- the VM would boot to disk I/O errors with no partition table. Convert it to uncompressed and start?\n%1$s Convert & start Start anyway Start anyway (%1$d) @@ -660,11 +660,11 @@ File icon Download in progress A download is still in progress. Leaving now will cancel it and delete the partial file. Are you sure? - Paused… - Paused, waiting for network… - Connection interrupted, retrying… - Paused, waiting for Wi-Fi… - Connection lost, retrying (%1$d/%2$d)… + Paused... + Paused, waiting for network... + Connection interrupted, retrying... + Paused, waiting for Wi-Fi... + Connection lost, retrying (%1$d/%2$d)... A download is already in progress. Please wait for it to finish. Download continues in the background. Check the notification shade. Could not start the download. Make sure the system Download Manager is enabled. @@ -676,7 +676,7 @@ Failed to download \"%1$s\". Downloading Ongoing download progress - Connecting… + Connecting... Cancel Suspended @@ -811,7 +811,7 @@ Secondary IPv6 networks (comma separated) DNS servers (empty = follow system DNS) Subnets %1$s and %2$s overlap - %1$s · %2$s + %1$s | %2$s Pseudo-bridge (pbridge) bridgedhcp gvswitch @@ -850,7 +850,7 @@ Auto Pseudo-bridge VM DHCP-PD - Searching… + Searching... Uplink unavailable Release Renew diff --git a/app/src/main/rust/netbox/Cargo.toml b/app/src/main/rust/netbox/Cargo.toml index e166ce64b..b744c6691 100644 --- a/app/src/main/rust/netbox/Cargo.toml +++ b/app/src/main/rust/netbox/Cargo.toml @@ -3,7 +3,7 @@ name = "netbox" version = "0.1.0" edition = "2021" rust-version = "1.91" -description = "netbox: a minimal netlink CLI toolbox for DroidVM — replaces the iproute2/bridge subset the daemon shells out to, so the APK doesn't depend on each OEM's system ip/bridge (versions vary; some don't even emit IFA_RT_PRIORITY in -j JSON)" +description = "netbox: a minimal netlink CLI toolbox for DroidVM -- replaces the iproute2/bridge subset the daemon shells out to, so the APK doesn't depend on each OEM's system ip/bridge (versions vary; some don't even emit IFA_RT_PRIORITY in -j JSON)" [[bin]] name = "netbox" diff --git a/app/src/main/rust/netbox/build.sh b/app/src/main/rust/netbox/build.sh index 3b3dc9690..267f015c9 100755 --- a/app/src/main/rust/netbox/build.sh +++ b/app/src/main/rust/netbox/build.sh @@ -6,7 +6,7 @@ # ./build.sh arm64 # aarch64-unknown-linux-musl -> dist/netbox-android-arm64 # ./build.sh host # quick host debug build (./target/debug/netbox) # -# Both release targets are fully static (musl), so they run under any libc — +# Both release targets are fully static (musl), so they run under any libc -- # glibc, musl, or Android bionic. The aarch64 target links with the bundled # rust-lld, so no external cross toolchain is needed. set -euo pipefail diff --git a/app/src/main/rust/netbox/src/main.rs b/app/src/main/rust/netbox/src/main.rs index 60f704023..7e7b818d9 100644 --- a/app/src/main/rust/netbox/src/main.rs +++ b/app/src/main/rust/netbox/src/main.rs @@ -1,7 +1,7 @@ -//! netbox — minimal netlink CLI for the DroidVM daemon. +//! netbox -- minimal netlink CLI for the DroidVM daemon. //! //! The daemon used to shell out to the system `ip`/`bridge` (iproute2). Those -//! vary per OEM and some are too old to emit `IFA_RT_PRIORITY` in `-j` JSON — +//! vary per OEM and some are too old to emit `IFA_RT_PRIORITY` in `-j` JSON -- //! exactly the field pbridge tags its offload-proxy addresses with. netbox does //! the same operations straight over rtnetlink, with our own JSON schema, so //! behaviour is identical on every device. Most invocations do one op and @@ -128,7 +128,7 @@ enum Cmd { }, /// Print the phone's own reachable IPv4 addresses once as `{"v4":[...]}`. HostIps { - /// Interface-name prefix to include (repeatable): wlan, rmnet_data, … + /// Interface-name prefix to include (repeatable): wlan, rmnet_data, ... #[arg(long)] iface: Vec, /// Drop addresses tagged with this IFA_RT_PRIORITY (pbridge offload). @@ -174,7 +174,7 @@ fn main() { fn run() -> Result<()> { let cli = Cli::parse(); - // tuntap is a plain ioctl — no netlink runtime needed. + // tuntap is a plain ioctl -- no netlink runtime needed. if let Cmd::LinkAddTap { name } = &cli.cmd { return tap::add_tap(name); } @@ -250,7 +250,7 @@ fn print_json(rows: &T) -> Result<()> { Ok(()) } -/// One JSON line `{"v4":[...]}` flushed immediately — the daemon reads these +/// One JSON line `{"v4":[...]}` flushed immediately -- the daemon reads these /// from the monitor's stdout, so the flush is what makes a change visible /// without waiting on stdio block-buffering. fn print_host_ips(v4: &[String]) -> Result<()> { diff --git a/app/src/main/rust/netbox/src/nl.rs b/app/src/main/rust/netbox/src/nl.rs index 5fabf20b7..6e2ecc8ce 100644 --- a/app/src/main/rust/netbox/src/nl.rs +++ b/app/src/main/rust/netbox/src/nl.rs @@ -28,12 +28,12 @@ pub struct Net { /// The only rtnetlink multicast group we ever join: RTM_NEWADDR/RTM_DELADDR /// for IPv4. The host-IP set is *defined* as v4 addresses, so only address /// events can change it; staying off the link/route/neigh/v6-addr groups -/// (all noisy and irrelevant) keeps daemon wakeups — and power — to a minimum. +/// (all noisy and irrelevant) keeps daemon wakeups -- and power -- to a minimum. const RTNLGRP_IPV4_IFADDR: u32 = 5; // ----------------------------------------------------------------- query rows -/// `metric` is IFA_RT_PRIORITY — the field the system `ip -j` may not emit but +/// `metric` is IFA_RT_PRIORITY -- the field the system `ip -j` may not emit but /// pbridge tags its proxy addresses with; netlink always carries it. #[derive(Serialize)] pub struct AddrRow { @@ -355,7 +355,7 @@ impl Net { /// `ip [-4|-6] rule add from all iif lookup

` (idempotent). /// `.v4()`/`.v6()` yield distinct typed builders, so the family branch - /// can't be factored out (it returns different types) — inline like pbridge. + /// can't be factored out (it returns different types) -- inline like pbridge. pub async fn rule_add(&self, v6: bool, iif: &str, table: u32) -> Result<()> { let req = self .handle @@ -500,7 +500,7 @@ impl Net { prefixes: &[String], exclude_metric: Option, ) -> Result> { - // one link dump → ifindex's name and whether it's a bridge + // one link dump -> ifindex's name and whether it's a bridge let mut names: HashMap = HashMap::new(); let mut is_bridge: HashMap = HashMap::new(); let mut ls = self.handle.link().get().execute(); @@ -735,7 +735,7 @@ pub async fn monitor_host_ipv4( let mut last = net.host_ipv4(prefixes, exclude_metric).await?; emit(&last)?; // every message arrives on RTNLGRP_IPV4_IFADDR, so any of them is a v4 - // address add/del — no need to inspect it, just re-evaluate the set. + // address add/del -- no need to inspect it, just re-evaluate the set. while messages.next().await.is_some() { // coalesce a burst (a DHCP renew is a del+add, etc.) into one recompute let _ = tokio::time::timeout(std::time::Duration::from_millis(200), async { diff --git a/app/src/main/rust/netbox/src/tap.rs b/app/src/main/rust/netbox/src/tap.rs index c820677ff..5f0348642 100644 --- a/app/src/main/rust/netbox/src/tap.rs +++ b/app/src/main/rust/netbox/src/tap.rs @@ -1,12 +1,12 @@ -//! TAP creation. `ip tuntap add` is not a netlink op — it is a TUNSETIFF ioctl -//! on /dev/net/tun — so it lives apart from the rtnetlink helpers. +//! TAP creation. `ip tuntap add` is not a netlink op -- it is a TUNSETIFF ioctl +//! on /dev/net/tun -- so it lives apart from the rtnetlink helpers. use anyhow::{bail, Context, Result}; use std::ffi::CString; use std::os::fd::AsRawFd; const IFNAMSIZ: usize = 16; -// — _IOW('T', 202/203, int). The ioctl request arg is +// -- _IOW('T', 202/203, int). The ioctl request arg is // c_ulong on glibc but c_int on musl/bionic, so cast with `as _` at the call. const TUNSETIFF: u32 = 0x4004_54ca; const TUNSETPERSIST: u32 = 0x4004_54cb; diff --git a/app/src/test/java/cn/classfun/droidvm/lib/store/network/VlanConfigPdTest.java b/app/src/test/java/cn/classfun/droidvm/lib/store/network/VlanConfigPdTest.java index 073acfcce..85531e9a8 100644 --- a/app/src/test/java/cn/classfun/droidvm/lib/store/network/VlanConfigPdTest.java +++ b/app/src/test/java/cn/classfun/droidvm/lib/store/network/VlanConfigPdTest.java @@ -11,7 +11,7 @@ /** * PD route-table persistence on the VLAN config: the id is bridgedhcp's * crash-cleanup key, so a value written through one wrapper must be seen - * by every later read of the same stored item — that is what makes the + * by every later read of the same stored item -- that is what makes the * allocation stable across network restarts. */ public class VlanConfigPdTest { From e01b8f8ea873eaaa0130690db7887913c8905cfa Mon Sep 17 00:00:00 2001 From: HuJK Date: Mon, 15 Jun 2026 14:00:01 +0800 Subject: [PATCH 016/182] fix(UI): fix clipboard, add font Terminal copy/paste was a no-op: SimpleTerminalSessionClient's onCopyTextToClipboard/onPasteTextFromClipboard were empty, so Termux's selection 'Copy'/'Paste' never reached the system clipboard. Implement both (reusing CopyableField for copy, emulator.paste() for paste) and pass an owning Context to the client at all three terminal screens (VM console, disk op, agent op). Also default the terminals to Maple Mono NL NF via TerminalView.setTypeface, with a Termux-compatible $HOME/.termux/font.ttf user override and a system-monospace fallback (TerminalFonts). The ~2.3MB TTF is git-ignored and fetched in build.sh (pinned v7.9, sha256-verified); only the OFL license is committed. Co-Authored-By: Claude Opus 4.8 --- .gitignore | 4 + app/src/main/assets/fonts/LICENSE.txt | 94 +++++++++++++++++++ .../termux/SimpleTerminalSessionClient.java | 30 ++++++ .../droidvm/lib/ui/termux/TerminalFonts.java | 65 +++++++++++++ .../ui/agent/AgentOperationActivity.java | 4 +- .../disk/operation/DiskOperationActivity.java | 4 +- .../ui/vm/console/VMConsoleActivity.java | 33 ++++++- build.sh | 35 +++++++ 8 files changed, 264 insertions(+), 5 deletions(-) create mode 100644 app/src/main/assets/fonts/LICENSE.txt create mode 100644 app/src/main/java/cn/classfun/droidvm/lib/ui/termux/TerminalFonts.java diff --git a/.gitignore b/.gitignore index 58b0eb928..722684c7e 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,7 @@ local.properties /app/src/main/assets/bin/*/lbx /app/src/main/assets/bin/*/netbox /app/src/main/assets/bin/*/pbridge + +# Terminal font: build.sh fetches the ~2.3MB Maple Mono NL NF TTF on first +# build; only the OFL license text under app/src/main/assets/fonts is committed. +/app/src/main/assets/fonts/*.ttf diff --git a/app/src/main/assets/fonts/LICENSE.txt b/app/src/main/assets/fonts/LICENSE.txt new file mode 100644 index 000000000..3e68033cd --- /dev/null +++ b/app/src/main/assets/fonts/LICENSE.txt @@ -0,0 +1,94 @@ +Copyright (c) 2022, subframe7536 (https://github.com/subframe7536), +with Reserved Font Name Maple Mono. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/app/src/main/java/cn/classfun/droidvm/lib/ui/termux/SimpleTerminalSessionClient.java b/app/src/main/java/cn/classfun/droidvm/lib/ui/termux/SimpleTerminalSessionClient.java index 1e150682b..07e471c3e 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/ui/termux/SimpleTerminalSessionClient.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/ui/termux/SimpleTerminalSessionClient.java @@ -1,5 +1,7 @@ package cn.classfun.droidvm.lib.ui.termux; +import android.content.ClipboardManager; +import android.content.Context; import android.util.Log; import androidx.annotation.NonNull; @@ -8,7 +10,25 @@ import com.termux.terminal.TerminalSession; import com.termux.terminal.TerminalSessionClient; +import cn.classfun.droidvm.lib.ui.CopyableField; + public abstract class SimpleTerminalSessionClient implements TerminalSessionClient { + /** Clipboard label for text copied out of a terminal. */ + private static final String CLIP_LABEL = "Terminal"; + + /** + * Used to reach the system clipboard for copy/paste; never null. Stored as + * given (typically the owning Activity) and only touched at copy/paste time, + * so this is safe even when constructed as an Activity field initializer + * (before the base context is attached). The client lives no longer than its + * owner, so retaining an Activity reference here leaks nothing. + */ + protected final Context context; + + protected SimpleTerminalSessionClient(@NonNull Context context) { + this.context = context; + } + @Override public void onTextChanged(@NonNull TerminalSession s) { } @@ -23,10 +43,20 @@ public void onSessionFinished(@NonNull TerminalSession s) { @Override public void onCopyTextToClipboard(@NonNull TerminalSession s, String t) { + if (t == null || t.isEmpty()) return; + CopyableField.copy(context, t, CLIP_LABEL); } @Override public void onPasteTextFromClipboard(@NonNull TerminalSession s) { + var cm = context.getSystemService(ClipboardManager.class); + if (cm == null || !cm.hasPrimaryClip()) return; + var clip = cm.getPrimaryClip(); + if (clip == null || clip.getItemCount() == 0) return; + var text = clip.getItemAt(0).coerceToText(context); + if (text == null || text.length() == 0) return; + var emulator = s.getEmulator(); + if (emulator != null) emulator.paste(text.toString()); } @Override diff --git a/app/src/main/java/cn/classfun/droidvm/lib/ui/termux/TerminalFonts.java b/app/src/main/java/cn/classfun/droidvm/lib/ui/termux/TerminalFonts.java new file mode 100644 index 000000000..d52fe68c7 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/lib/ui/termux/TerminalFonts.java @@ -0,0 +1,65 @@ +package cn.classfun.droidvm.lib.ui.termux; + +import android.content.Context; +import android.graphics.Typeface; +import android.util.Log; + +import androidx.annotation.NonNull; + +import com.termux.view.TerminalView; + +import java.io.File; + +/** + * Picks the typeface for the embedded terminals. Mirrors Termux's font logic: + * a user-supplied {@code $HOME/.termux/font.ttf} wins, otherwise the bundled + * Maple Mono NL NF is used, falling back to the system monospace if either fails + * to parse. The console shells run with {@code HOME=getFilesDir()}, so the + * user-override path matches Termux exactly. + */ +public final class TerminalFonts { + private static final String TAG = "TerminalFonts"; + + /** Bundled default: Maple Mono NL NF (NL = no ligatures); see app/src/main/assets/fonts. */ + private static final String ASSET_FONT = "fonts/MapleMonoNL-NF-Regular.ttf"; + + /** Termux-compatible drop-in override, relative to the shell's HOME. */ + private static final String USER_FONT = ".termux/font.ttf"; + + /** The bundled font is immutable, so parse it once and reuse. */ + private static Typeface bundled; + + private TerminalFonts() { + } + + /** Loads {@link #load(Context)} and applies it to {@code tv}. */ + public static void apply(@NonNull TerminalView tv) { + tv.setTypeface(load(tv.getContext())); + } + + /** + * Resolves the terminal typeface. The user override is re-checked on every + * call (a cheap stat) so dropping in a {@code font.ttf} takes effect the + * next time a terminal is opened, without restarting the app. + */ + @NonNull + public static Typeface load(@NonNull Context ctx) { + var userFont = new File(ctx.getFilesDir(), USER_FONT); + if (userFont.isFile()) { + try { + return Typeface.createFromFile(userFont); + } catch (Exception e) { + Log.w(TAG, "Ignoring unreadable user font " + userFont, e); + } + } + if (bundled == null) { + try { + bundled = Typeface.createFromAsset(ctx.getAssets(), ASSET_FONT); + } catch (Exception e) { + Log.w(TAG, "Failed to load bundled terminal font, using monospace", e); + bundled = Typeface.MONOSPACE; + } + } + return bundled; + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/ui/agent/AgentOperationActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/agent/AgentOperationActivity.java index 3b38badac..83f4b35be 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/agent/AgentOperationActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/agent/AgentOperationActivity.java @@ -39,6 +39,7 @@ import cn.classfun.droidvm.lib.daemon.ForegroundCallback; import cn.classfun.droidvm.lib.store.disk.DiskStore; import cn.classfun.droidvm.lib.ui.termux.SimpleTerminalSessionClient; +import cn.classfun.droidvm.lib.ui.termux.TerminalFonts; import cn.classfun.droidvm.lib.ui.termux.SimpleTerminalViewClient; import cn.classfun.droidvm.ui.agent.base.AgentVM; import cn.classfun.droidvm.ui.agent.base.BaseAction; @@ -61,7 +62,7 @@ public final class AgentOperationActivity extends AppCompatActivity private AgentVM agentVM = null; private BaseAction action = null; - private final SimpleTerminalSessionClient sessionClient = new SimpleTerminalSessionClient() { + private final SimpleTerminalSessionClient sessionClient = new SimpleTerminalSessionClient(this) { @Override public void onTextChanged(@NonNull TerminalSession s) { mainHandler.post(() -> { @@ -148,6 +149,7 @@ private void initTerminal() { terminalSession = new TerminalSession(shell, cwd, args, env, null, sessionClient); float density = getResources().getDisplayMetrics().density; terminalView.setTextSize((int) (4 * density)); + TerminalFonts.apply(terminalView); terminalView.attachSession(terminalSession); } diff --git a/app/src/main/java/cn/classfun/droidvm/ui/disk/operation/DiskOperationActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/disk/operation/DiskOperationActivity.java index f641f5669..12a3b1e54 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/disk/operation/DiskOperationActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/disk/operation/DiskOperationActivity.java @@ -40,6 +40,7 @@ import cn.classfun.droidvm.lib.store.disk.DiskConfig; import cn.classfun.droidvm.lib.store.disk.DiskStore; import cn.classfun.droidvm.lib.ui.termux.SimpleTerminalSessionClient; +import cn.classfun.droidvm.lib.ui.termux.TerminalFonts; import cn.classfun.droidvm.lib.ui.termux.SimpleTerminalViewClient; public final class DiskOperationActivity extends AppCompatActivity { @@ -67,7 +68,7 @@ public final class DiskOperationActivity extends AppCompatActivity { private DiskStore diskStore = null; private DiskConfig diskConfig = null; - private final TerminalSessionClient sessionClient = new SimpleTerminalSessionClient() { + private final TerminalSessionClient sessionClient = new SimpleTerminalSessionClient(this) { @Override public void onTextChanged(@NonNull TerminalSession s) { mainHandler.post(() -> { @@ -246,6 +247,7 @@ private void startTerminalSession(String cmd) { session = new TerminalSession(shell, cwd, args, env, null, sessionClient); float density = getResources().getDisplayMetrics().density; terminalView.setTextSize((int) (10 * density)); + TerminalFonts.apply(terminalView); terminalView.attachSession(session); } diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/console/VMConsoleActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/console/VMConsoleActivity.java index d5306c6e1..ef0c73806 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/console/VMConsoleActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/console/VMConsoleActivity.java @@ -50,6 +50,7 @@ import cn.classfun.droidvm.lib.daemon.DaemonConnection; import cn.classfun.droidvm.lib.ui.ImeInsetsExempt; import cn.classfun.droidvm.lib.ui.termux.SimpleTerminalSessionClient; +import cn.classfun.droidvm.lib.ui.termux.TerminalFonts; import cn.classfun.droidvm.lib.ui.termux.SimpleTerminalViewClient; import cn.classfun.droidvm.lib.utils.ShareUtils; @@ -60,6 +61,11 @@ public final class VMConsoleActivity extends AppCompatActivity implements ImeIns public static final String EXTRA_STREAM = "stream"; public static final String EXTRA_LOGS = "logs"; private static final String DEFAULT_STREAM = "uart"; + private static final String PREFS_NAME = "droidvm_prefs"; + private static final String KEY_FONT_SIZE = "console_font_size"; + private static final float MIN_FONT_SIZE = 2; + private static final float MAX_FONT_SIZE = 48; + private static final float DEFAULT_FONT_SIZE = 5; private final Handler mainHandler = new Handler(Looper.getMainLooper()); private ActivityResultLauncher saveLogLauncher; private TerminalView terminalView; @@ -70,7 +76,7 @@ public final class VMConsoleActivity extends AppCompatActivity implements ImeIns public String vmName; public String streamName; - private final TerminalSessionClient sessionClient = new SimpleTerminalSessionClient() { + private final TerminalSessionClient sessionClient = new SimpleTerminalSessionClient(this) { @Override public void onTextChanged(@NonNull TerminalSession s) { mainHandler.post(() -> { @@ -80,12 +86,12 @@ public void onTextChanged(@NonNull TerminalSession s) { } }; - private float currentFontSize = 5; + private float currentFontSize = DEFAULT_FONT_SIZE; private final TerminalViewClient viewClient = new SimpleTerminalViewClient() { @Override public float onScale(float scale) { var dampened = 1.0f + (scale - 1.0f) * 0.1f; - currentFontSize = Math.max(2, Math.min(48, currentFontSize * dampened)); + currentFontSize = Math.max(MIN_FONT_SIZE, Math.min(MAX_FONT_SIZE, currentFontSize * dampened)); if (terminalView != null) { var density = getResources().getDisplayMetrics().density; terminalView.setTextSize((int) (currentFontSize * density)); @@ -158,11 +164,13 @@ protected void onCreate(Bundle savedInstanceState) { "PATH=/system/bin", fmt("HOME=%s", cwd), }; + currentFontSize = loadFontSize(); var density = getResources().getDisplayMetrics().density; var session = new TerminalSession(shell, cwd, args, env, null, sessionClient); terminalSession = session; terminalView.attachSession(session); terminalView.setTextSize((int) (currentFontSize * density)); + TerminalFonts.apply(terminalView); terminalView.setFocusable(true); terminalView.setFocusableInTouchMode(true); terminalView.requestFocus(); @@ -184,6 +192,25 @@ private boolean onMenuItemClicked(@NonNull MenuItem item) { return false; } + @Override + protected void onPause() { + super.onPause(); + saveFontSize(); + } + + private float loadFontSize() { + var saved = getSharedPreferences(PREFS_NAME, MODE_PRIVATE) + .getFloat(KEY_FONT_SIZE, DEFAULT_FONT_SIZE); + return Math.max(MIN_FONT_SIZE, Math.min(MAX_FONT_SIZE, saved)); + } + + private void saveFontSize() { + getSharedPreferences(PREFS_NAME, MODE_PRIVATE) + .edit() + .putFloat(KEY_FONT_SIZE, currentFontSize) + .apply(); + } + @Override protected void onDestroy() { super.onDestroy(); diff --git a/build.sh b/build.sh index 8794ab536..4fbd496ea 100755 --- a/build.sh +++ b/build.sh @@ -142,6 +142,39 @@ build_netbox() { install -m755 app/src/main/rust/netbox/dist/netbox-linux-x64 app/src/main/assets/bin/x86_64/netbox } +# ------------------------------------------------------------- terminal font +# Maple Mono NL NF (no-ligature Nerd Font) is the default terminal typeface, +# applied at runtime via TerminalView.setTypeface (see TerminalFonts.java). The +# ~2.3MB TTF is git-ignored and fetched here on first build; only the OFL +# license text is committed. Users can still override it with +# $HOME/.termux/font.ttf, and the app falls back to the system monospace when +# the font is absent -- so a download failure is a warning, not fatal. +FONT_VER="v7.9" +FONT_DIR="app/src/main/assets/fonts" +FONT_TTF="$FONT_DIR/MapleMonoNL-NF-Regular.ttf" +FONT_SHA="aa3b096bc92df8503d77482b285a0567bafa6e83230d969700f455e610b1f655" +FONT_URL="https://github.com/subframe7536/maple-font/releases/download/$FONT_VER/MapleMonoNL-NF.zip" + +font_ok() { [ -f "$FONT_TTF" ] && echo "$FONT_SHA $FONT_TTF" | sha256sum -c - >/dev/null 2>&1; } + +fetch_font() { + if font_ok; then return 0; fi + if ! have curl || ! have unzip; then + warn "curl/unzip missing; cannot fetch terminal font (app uses system monospace)" + return 0 + fi + log "Fetching terminal font Maple Mono NL NF ($FONT_VER)" + mkdir -p "$FONT_DIR" + local tmp; tmp="$(mktemp -d)" + if curl -fsSL --max-time 180 -o "$tmp/nf.zip" "$FONT_URL" \ + && unzip -o -j "$tmp/nf.zip" '*NL-NF-Regular.ttf' -d "$FONT_DIR" >/dev/null; then + font_ok || warn "terminal font sha256 mismatch (upstream re-released?); keeping it anyway" + else + warn "failed to fetch terminal font (app uses system monospace)" + fi + rm -rf "$tmp" +} + if [ "${SKIP_THIRDPARTY:-0}" = "1" ]; then log "Skipping third-party builds (SKIP_THIRDPARTY=1)" for bin in gvswitch pbridge bridgedhcp lbx netbox; do @@ -155,6 +188,8 @@ else build_netbox fi +fetch_font + # --------------------------------------------------------------------- APK log "Building APK ($GRADLE_TASK)" ./gradlew "$GRADLE_TASK" From fee903d6d79798b4aeff95d6af3b6b1b4f725e79 Mon Sep 17 00:00:00 2001 From: HuJK Date: Thu, 18 Jun 2026 23:41:08 +0800 Subject: [PATCH 017/182] feat(build): replace 7za with XZ for Java; source native prebuilts from DroidVM-Prebuilt-Root - Drop bundled 7za; decompress the runtime payload in-process with org.tukaani:xz + a minimal USTAR reader (AssetUtils). - Runtime prebuilt is prebuilt-.tar.xz; the third-party daemons + lbx ship inside it (DATA/bin/), so the 7za/thirdparty asset extraction and the BINARIES_PREBUILT/THIRDPARTY constants are gone. Stale assets/bin daemons removed; *.7z and *-comptime.zip excluded from APK assets. - lbx also ships in prebuilt--comptime.zip, unpacked at Gradle build time into lib//liblbx.so (UnpackComptimeJniLibsTask). - Terminal font fetched by a Gradle task (FetchTerminalFontTask) so CI and local builds match; build.sh slimmed to preflight + submodules + gradle, with a native-dev toolchain check when DroidVM-Prebuilt-Root is present. - regenPrebuilts runs auto-build.py when DroidVM-Prebuilt-Root is checked out. - Drop x86_64 from abiFilters (no x86_64 VM runtime exists). - Workflows: add update-latest-release.yml; add manual workflow_dispatch to build.yml and release.yml. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/build.yml | 1 + .github/workflows/dev-release.yml | 93 +++ .github/workflows/release.yml | 1 + .gitignore | 9 +- app/build.gradle.kts | 176 +++- app/src/main/assets/bin/arm64-v8a/7za | Bin 2439344 -> 0 bytes app/src/main/assets/bin/x86_64/7za | Bin 2711048 -> 0 bytes app/src/main/assets/data/license.yaml | 8 +- app/src/main/assets/prebuilts | 2 +- .../cn/classfun/droidvm/lib/Constants.java | 5 +- .../droidvm/lib/utils/AssetUtils.java | 165 +++- app/src/main/rust/netbox/.gitignore | 2 - app/src/main/rust/netbox/Cargo.lock | 574 ------------- app/src/main/rust/netbox/Cargo.toml | 27 - app/src/main/rust/netbox/build.sh | 40 - app/src/main/rust/netbox/src/main.rs | 263 ------ app/src/main/rust/netbox/src/nl.rs | 762 ------------------ app/src/main/rust/netbox/src/tap.rs | 48 -- build.sh | 210 ++--- gradle/libs.versions.toml | 2 + 20 files changed, 443 insertions(+), 1945 deletions(-) create mode 100644 .github/workflows/dev-release.yml delete mode 100644 app/src/main/assets/bin/arm64-v8a/7za delete mode 100644 app/src/main/assets/bin/x86_64/7za delete mode 100644 app/src/main/rust/netbox/.gitignore delete mode 100644 app/src/main/rust/netbox/Cargo.lock delete mode 100644 app/src/main/rust/netbox/Cargo.toml delete mode 100755 app/src/main/rust/netbox/build.sh delete mode 100644 app/src/main/rust/netbox/src/main.rs delete mode 100644 app/src/main/rust/netbox/src/nl.rs delete mode 100644 app/src/main/rust/netbox/src/tap.rs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 24ba99086..8cbc9c64b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,6 +5,7 @@ on: branches: [ "**" ] pull_request: branches: [ "**" ] + workflow_dispatch: concurrency: group: build-${{ github.ref }} diff --git a/.github/workflows/dev-release.yml b/.github/workflows/dev-release.yml new file mode 100644 index 000000000..e0f5772a1 --- /dev/null +++ b/.github/workflows/dev-release.yml @@ -0,0 +1,93 @@ +name: Dev release + +# Every push to the default branch (re)publishes a rolling pre-release on a +# dedicated `dev` tag. The tag is a moving label -- it is deleted and recreated +# at the current commit each run, so it never pins a specific version. A GitHub +# Release must be attached to a tag, hence the dummy `dev` tag. +# (release.yml still cuts real, versioned releases on real tag pushes.) +on: + push: + branches: [ master ] + workflow_dispatch: + +permissions: + contents: write + +concurrency: + group: dev-release + cancel-in-progress: true + +jobs: + dev-release: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive + fetch-depth: 0 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + + - name: Setup Android SDK + uses: android-actions/setup-android@v3 + with: + packages: build-tools;36.0.0 + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Build release APK + run: ./gradlew assembleRelease + + - name: Sign release APK + if: ${{ env.HAS_SIGN_KEY == 'true' }} + env: + HAS_SIGN_KEY: ${{ secrets.APP_SIGN_KEY_JKS != '' }} + APP_SIGN_KEY_JKS: ${{ secrets.APP_SIGN_KEY_JKS }} + APP_SIGN_KEY_PASS: ${{ secrets.APP_SIGN_KEY_PASS }} + run: | + BUILD_TOOLS="${ANDROID_HOME}/build-tools/36.0.0" + echo "$APP_SIGN_KEY_JKS" | base64 -d > /tmp/keystore.jks + UNSIGNED=$(find app/build/outputs/apk/release -name '*.apk' | head -1) + ALIGNED="${UNSIGNED%.apk}-aligned.apk" + "${BUILD_TOOLS}/zipalign" -f -v 4 "$UNSIGNED" "$ALIGNED" + "${BUILD_TOOLS}/apksigner" sign \ + --ks /tmp/keystore.jks \ + --ks-pass "pass:${APP_SIGN_KEY_PASS}" \ + --key-pass "pass:${APP_SIGN_KEY_PASS}" \ + --out "$UNSIGNED" \ + "$ALIGNED" + rm -f "$ALIGNED" /tmp/keystore.jks + + - name: Stage APK + run: | + set -euo pipefail + mkdir -p release + cp app/build/outputs/apk/release/*.apk "release/DroidVM-dev.apk" + + - name: Publish rolling dev pre-release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + ver="$(git describe --tags --always 2>/dev/null || git rev-parse --short HEAD)" + + # Drop the previous dev release + tag, then recreate at this commit so + # `dev` always points at latest master (a moving, version-less tag). + gh release delete dev --yes --cleanup-tag 2>/dev/null || true + git push origin :refs/tags/dev 2>/dev/null || true + + gh release create dev "release/DroidVM-dev.apk" \ + --prerelease \ + --target "${GITHUB_SHA}" \ + --title "DroidVM dev (${ver})" \ + --notes "Rolling development build from ${GITHUB_SHA} (${ver}). Not a stable release." diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 78ca65c98..ed0159217 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,6 +3,7 @@ name: Release on: push: tags: [ "**" ] + workflow_dispatch: permissions: contents: write diff --git a/.gitignore b/.gitignore index 722684c7e..51138b689 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ /local.properties /.idea /.vscode +/DroidVM-Prebuilt-Root .DS_Store /build /captures @@ -10,9 +11,13 @@ .cxx local.properties -# build.sh stages the compiled network/boot binaries next to the committed -# 7za prebuilt; only 7za is tracked, the rest are build output. +# Legacy native-util source locations -- the daemons + lbx are now built by the +# DroidVM-Prebuilt-Root pipeline, not staged into the app tree. /app/src/thirdparty +/app/src/main/rust + +# Stale daemon binaries from the old build.sh staging (now delivered via the +# prebuilt tar.xz); ignore leftovers so they are never packaged or committed. /app/src/main/assets/bin/*/bridgedhcp /app/src/main/assets/bin/*/gvswitch /app/src/main/assets/bin/*/lbx diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2c6824937..d0cf93ad1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,5 +1,8 @@ @file:Suppress("UnstableApiUsage") +import java.net.URI +import java.security.MessageDigest + plugins { alias(libs.plugins.android.application) } @@ -26,8 +29,9 @@ val gitShortSha = runGit("rev-parse", "--short", "HEAD") // describe fails on a checkout without tags (shallow clone, or tags not fetched); // fall back to a 0.0.0 base so the build still works and the version is clearly -// marked as tag-less. -val gitDescribe = (runGitOrNull("describe", "--long", "--tags") +// marked as tag-less. Exclude the rolling `dev` tag (dev-release.yml recreates it +// at HEAD each push) so it never shadows real version tags in the version name. +val gitDescribe = (runGitOrNull("describe", "--long", "--tags", "--exclude=dev") ?: "0.0.0-$gitCommitCount-g$gitShortSha") .removePrefix("v").removePrefix("V") val generatedVersionName: String = if (gitDescribe.matches(Regex(".*-0-g[0-9a-f]+$"))) { @@ -60,7 +64,9 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" ndk { - abiFilters += listOf("arm64-v8a", "x86_64") + // arm64-v8a only: the VM runtime prebuilt (crosvm/qemu/kernel) ships + // for arm64 alone, so x86_64 could never boot a guest anyway. + abiFilters += listOf("arm64-v8a") } externalNativeBuild { cmake { @@ -103,6 +109,14 @@ android { useLegacyPackaging = true } } + androidResources { + // prebuilt--comptime.zip is a build-time input only: Gradle unpacks + // it into jniLibs (lib//liblbx.so). Never ship it as an APK asset. + // Also exclude the legacy *.7z (replaced by *.tar.xz) so a stale archive + // lingering in the prebuilts submodule can't double the APK size. The + // runtime prebuilt-.tar.xz + .json stay packaged as assets. + ignoreAssetsPatterns += listOf("*-comptime.zip", "*.7z") + } } abstract class CopyNativeBinAssetsTask : DefaultTask() { @@ -139,46 +153,163 @@ abstract class CopyNativeBinAssetsTask : DefaultTask() { } } -// Ship lbx as `liblbx.so` in jniLibs so the (non-root) UI process can exec it -// from its nativeLibraryDir -- the data-dir copy used by the daemon isn't -// executable by the app's untrusted_app SELinux domain. -abstract class CopyLbxJniLibsTask : DefaultTask() { +// Unpack each prebuilt--comptime.zip from the prebuilts submodule into +// jniLibs//. These are files that must be executable from the app's +// nativeLibraryDir (currently liblbx.so) -- the data-dir copy extracted from the +// runtime tar.xz isn't executable by the app's untrusted_app SELinux domain. +// Build-time extraction (unlike a first-run extract) is fine because the package +// installer, not the app, populates nativeLibraryDir. +abstract class UnpackComptimeJniLibsTask : DefaultTask() { + @get:javax.inject.Inject + abstract val archives: org.gradle.api.file.ArchiveOperations + + @get:javax.inject.Inject + abstract val fs: org.gradle.api.file.FileSystemOperations + @get:InputDirectory - abstract val distDir: DirectoryProperty + abstract val prebuiltsDir: DirectoryProperty @get:OutputDirectory abstract val outputDir: DirectoryProperty @TaskAction - fun copy() { + fun unpack() { val outDir = outputDir.get().asFile outDir.deleteRecursively() - val dist = distDir.get().asFile - if (!dist.exists()) return - dist.listFiles()?.filter { it.isDirectory }?.forEach { abiDir -> - val src = File(abiDir, "lbx") - if (src.isFile) { - val dest = File(outDir, "${abiDir.name}/liblbx.so") - dest.parentFile.mkdirs() - src.copyTo(dest, overwrite = true) + val prebuilts = prebuiltsDir.get().asFile + val re = Regex("^prebuilt-(.+)-comptime\\.zip$") + val zips = prebuilts.listFiles { f -> f.isFile && re.matches(f.name) } ?: return + for (zip in zips) { + val abi = re.find(zip.name)!!.groupValues[1] + fs.copy { + from(archives.zipTree(zip)) + into(File(outDir, abi)) + } + } + } +} + +// Native-dev hook: when DroidVM-Prebuilt-Root is checked out at the repo root +// with a non-empty auto-build/ (someone is editing a native util locally), +// regenerate the prebuilt-* artifacts into the prebuilts submodule before they +// are packaged. Pure-Java devs don't have this dir, so the CI-published +// submodule artifacts are used as-is (the task no-ops via onlyIf). +abstract class RegenPrebuiltsTask : DefaultTask() { + @get:javax.inject.Inject + abstract val exec: org.gradle.process.ExecOperations + + @get:org.gradle.api.tasks.Internal + abstract val prebuiltRoot: DirectoryProperty + + @get:org.gradle.api.tasks.Internal + abstract val prebuiltsOut: DirectoryProperty + + @TaskAction + fun regen() { + val root = prebuiltRoot.get().asFile + val out = prebuiltsOut.get().asFile + logger.lifecycle("DroidVM-Prebuilt-Root detected; regenerating prebuilts via auto-build.py") + exec.exec { + workingDir = root + commandLine("python3", "auto-build.py", "--out", out.absolutePath) + } + } +} + +// Fetch the terminal font (Maple Mono NL NF) at build time so CI (./gradlew) and +// local builds behave identically -- it used to live only in build.sh, which CI +// never runs. Download is best-effort: a failure logs a warning and the app falls +// back to the system monospace, so it never breaks the build. +abstract class FetchTerminalFontTask : DefaultTask() { + @get:javax.inject.Inject + abstract val archives: org.gradle.api.file.ArchiveOperations + + @get:org.gradle.api.tasks.Input + abstract val url: org.gradle.api.provider.Property + + @get:org.gradle.api.tasks.Input + abstract val sha256: org.gradle.api.provider.Property + + @get:OutputDirectory + abstract val outputDir: DirectoryProperty + + @TaskAction + fun fetch() { + val ttf = File(outputDir.get().asFile, "fonts/MapleMonoNL-NF-Regular.ttf") + if (ttf.isFile && sha256Hex(ttf) == sha256.get()) return + ttf.parentFile.mkdirs() + val tmpZip = File(temporaryDir, "font.zip") + try { + URI(url.get()).toURL().openStream().use { input -> + tmpZip.outputStream().use { input.copyTo(it) } } + } catch (e: Exception) { + logger.warn("Could not download terminal font (${e.message}); app uses system monospace") + return } + val src = archives.zipTree(tmpZip).matching { include("**/*NL-NF-Regular.ttf") } + .files.firstOrNull() + if (src == null) { + logger.warn("Terminal font zip had no NL-NF-Regular.ttf; app uses system monospace") + return + } + src.copyTo(ttf, overwrite = true) + if (sha256Hex(ttf) != sha256.get()) + logger.warn("Terminal font sha256 mismatch (upstream re-released?); keeping it anyway") + } + + private fun sha256Hex(f: File): String { + val md = MessageDigest.getInstance("SHA-256") + f.inputStream().use { ins -> + val buf = ByteArray(8192) + var n = ins.read(buf) + while (n >= 0) { md.update(buf, 0, n); n = ins.read(buf) } + } + return md.digest().joinToString("") { "%02x".format(it.toInt() and 0xFF) } + } +} + +val prebuiltRootDir = rootProject.layout.projectDirectory.dir("DroidVM-Prebuilt-Root") +val prebuiltsSubmoduleDir = rootProject.layout.projectDirectory.dir("app/src/main/assets/prebuilts") +val regenPrebuilts = tasks.register("regenPrebuilts") { + prebuiltRoot.set(prebuiltRootDir) + prebuiltsOut.set(prebuiltsSubmoduleDir) + // Only when a native dev has checked out DroidVM-Prebuilt-Root with source + // under auto-build/. Otherwise the published submodule artifacts are used. + onlyIf { + val autoBuild = prebuiltRootDir.dir("auto-build").asFile + prebuiltRootDir.file("auto-build.py").asFile.isFile && + autoBuild.isDirectory && + (autoBuild.listFiles()?.any { it.name != ".gitignore" } == true) } } +// Run before anything reads the prebuilts dir (assets merge + jniLibs unpack). +tasks.named("preBuild").configure { dependsOn(regenPrebuilts) } androidComponents { onVariants { variant -> val variantName = variant.name.replaceFirstChar { it.uppercase() } - val copyLbxJniTask = tasks.register( - "copyLbxJniLibs${variantName}" + val unpackComptimeTask = tasks.register( + "unpackComptimeJniLibs${variantName}" ) { - distDir.set(rootProject.layout.projectDirectory.dir("app/src/main/assets/bin")) + dependsOn(regenPrebuilts) + prebuiltsDir.set(prebuiltsSubmoduleDir) outputDir.set( - layout.buildDirectory.dir("generated/lbx_jnilibs/${variant.name}") + layout.buildDirectory.dir("generated/comptime_jnilibs/${variant.name}") ) } variant.sources.jniLibs?.addGeneratedSourceDirectory( - copyLbxJniTask, CopyLbxJniLibsTask::outputDir + unpackComptimeTask, UnpackComptimeJniLibsTask::outputDir + ) + val fetchFontTask = tasks.register( + "fetchTerminalFont${variantName}" + ) { + url.set("https://github.com/subframe7536/maple-font/releases/download/v7.9/MapleMonoNL-NF.zip") + sha256.set("aa3b096bc92df8503d77482b285a0567bafa6e83230d969700f455e610b1f655") + outputDir.set(layout.buildDirectory.dir("generated/font_assets/${variant.name}")) + } + variant.sources.assets?.addGeneratedSourceDirectory( + fetchFontTask, FetchTerminalFontTask::outputDir ) val copyNativeTask = tasks.register( "copyNativeBinAssets${variantName}" @@ -213,6 +344,7 @@ dependencies { implementation(libs.material) implementation(libs.okhttp3) implementation(libs.snakeyaml) + implementation(libs.xz) implementation(libs.termux.emulator) implementation(libs.termux.view) testImplementation(libs.junit) diff --git a/app/src/main/assets/bin/arm64-v8a/7za b/app/src/main/assets/bin/arm64-v8a/7za deleted file mode 100644 index 109880c35402ef0a352be86cf3ac13baafe39040..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2439344 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�(}+;yIL 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 z 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/p+XDGcnMZn>CzB7sgVBTR9)|pD;z3Dxkj80d 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 z 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%0DW8cA3tbWg$K4lU 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

M8A{q9V}^;Z!efb&3TjP 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 z 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 d>v>{TEo?hGO!Qgy6yS zA?B*5&FDGXzFOM+v|ZYqJ}kYt8o6~n{kGC?Be?N>t+ZL=@}_am2kmY#p59LMN-6m+ zKJsRjXio097a%+THi1}sU?SMKx>?^I;8RJveqd3V0-sjw;X?9MeSi5~yZ_Evg(kig zGi6C{EUfRdk^QP4U0B!0o$Y;o#;_c^X#^%Kq&LijhHcnG_1WgCsypx6U2i7NIqLB= z6Cc@9<@M+;wOvmArPL??fuHAY{YvDX7x))q3)tK%p!>15agMHPaG#k$Rh(?Lc z;m#4AH6P#cALPcWZJ638ePf=uqI`U2Lf@DthL?w-QRg`{GA`Q_>%N;auE;28d}EZ~ zBzh^`Qf;s;2IeDmb7x|Aw8+-+0jE&!9n(y1br^hKCEC?9(UH%k#awbtL$`WvAG~AQ zNAQ={IuV@%guh=zt~z=u$CRx@e@Mo5#^dD1pVaco9VcaNFxPByWvgU$+blaaDbH^A zq1R^QnRe`*m}D{X13S>f)~3o*p1U%-jdhOdN-h^trw!f%)~B^0k5#4)zOWd(1x)K` z!%JR?BJywO8-1~{Zj^~tLVHEL_wpM(#CPIdfN~ZewJ&%IClPpL9lG&Bay2CK{EmeW z#&`Vk&J$}CBP#wA!x5TCKS+pa4_9N~YiUM=V#1Q#>Boo)Z^jVoWduKu>HqnmaR5v20HUNip z)Yq8T@&8-A(^*jQeG&7PyojRbdf0391AAo0Ce3#SxUl&@30(MqD|w8^4wfk^{!hI0 zl=I`hg%wr7xwDy{+FB3ngqud%t1#YP_0!0I;iP`l%C+kmSLKKCO;04@H!?YI?tC9h zoPEhwzPp&eEuFww42RY>J8J~wDSDgx2uyTZOD$_Ok6FQ;S;U+JZ^K;LR~))%%7yc~U<8H_mnOOzqZ3hPCLOq5Mb5 z=fdlXiL8Suqp>(R<}QpOn*Si3^9=HHKp*xfr;o1GKB6P~`}lkEd6%EN8R9c0{74s`h|V^*irl~Wt2I}T8|uFQ`7aE`@#a$e~LP* zkj-~|wX9jX^)_@{6EvZA6L}^Q?dj~!a?e=D&N|B4U3=IA=Qr@b<`qEJr;gqJWTax| zpEpU`!`|tqy$Wd0GWY>Rcahwd8PMIU;A#MW1oK+_k(?IJ&9=;E9jti;Kl-dsaW{S3 zHvUq^s64k;iS~4yvwFZjeN>I^K#Uo$*|dc|SCWsYlK7@oIo09a#5vpsT$GE*#c_}| zMoQj|60_j8K4A353StD77n&C==IEZANR92=lbhDQgfe?_XK;TeF-FW~6>!&j`$f)M z4uFd%tE^G{#e43@Yp!Dqx!`2+SN;7s@j5t>tQ<|u=@@9I;!W9m1>Rf-Cp&51(JQCm z@y~^`FRx`!5%e={tQoXF4CdqWq_fGaY`pk@ZnJ<9gMkH|Mg( zU6udh6E{M`-_EhiUma$0o?#ui@4;Zs-pQ|y-%ZWu+>=DxE7n%V%r&{)xoYi`@Tn>D-c0!y8j=7cU?6Fm{%sJjNyxh2Y99by!GzPB>yLJ z?TEI1a5ei~%v-oG;XH1X_(!exJ-@GvH4Zi0qhpuX<(a7#bS`~R!aECkuY=A#(D~(- zr`da%WbM$7Vc)Ne)kA;pa?WS?#ns_kIES0Hp)xj%dfCwHm!P5Hv(IUkosh+ylm*uC zUhMsvbjuEtM`r4fpH;?ApM83B4)5=!pWhN$t~2Sgk#}cuhI+Xt zvv)l@<(_Oa^&a#;-~I+WI;+TgYQ#-{AZAe4owPU-FJLH!?1LuNZ1#D~6x+SsL=cNPd(7w_tigJ_U(C zbzz);o&3pTOT5ULS{c3DI9D!S6O8-; z@~UDV{tkXS9XLF7XWG1#*w8B{nw(bR)Uucd_H8-Oa`ar!bdUPez-tTeGW?#zywlQ6 z>FM-$H?e7HjQc6w!%jXXFTQ)^O3h?ow~{-#^-S@l4+4jk%&V2N`CZ%>G>LW5a+B8Z zU`yj#`6*RL@ZQH6b>UzIIYw3I9_mzZPJamh8=X29bylu@k~*8vyDp610mgz|>kr6{ z%ez*S*$^TJg2twC_K%Ne8rx+2T5X(-dRsWdPN+bSHulDAwxZMg=uK$U`83k*H&h0C zdz|l&*@IAD(lah^=v)3(WaoA5}=cefV%Mvaf5s&b@h^+)CcS z$QF$u$~m|x^rJG`%ZWnwY2Z!zzX)0o->n=~z0?No$SKDk$9k?@v}wS#?x+ruFHyM& z9GK`_5o-XIb-*`eYIS978@7EX|5N9t`FQIRb32YLta-f-4rHHM z$Q9jByMeh_J1b-AQ~wu0i}d@5lVgvugNy$B9Usmc(q0MAVfXA5Eb1e9oU2dR%f;VE zW-*onBTah`{<9e8BM*$U+tp_ecRI6Ywb5yJs$<)i2awtK7;}MeARG%H8m~(aZzRY2 z6UG|?H!F=PQSOuOb4^UK?aDJ52u^&X^BLVCr#!erxZ4t(r)8$H;QGMK*Y8FL zqgzVrZ?+$GWk4GJK=)zr))I&QhoMs)c!_UD4&)onQ}Of6$?<1&d(%s_U;cCEIfHf> z^R8p8_1w1g9j{L7&u!KI8=djbY-dc)_#1_v>Wnv*V-Fr*mfL=Odm3>DqgfLe^6z;{Tzs!U#{i?^ z&P-(Aygc@d#*i;yEV*;?+mF4~I$pB6XN`P)!ClCuxvgHa(3YITpZ$&DePw?m7qD!^ zPdK;!DR-3RXC-`Bf=?%Mpw?@B&<#CQt$J*2XFWW)-n1;lzbAPaMF&REC%3Whb}eV2 z1EMwXqJ7OSZxEfFyQ3D__z->?#Ti?ydtN?`ebqwt_psaWqsQLlu33!%`M*eG=#9*yq|~=wv)<`!BS++B2cq2d;l?`pTAj$M;@K zE+ge9@1#A+=rr01VaGUg!rpe~RFs%gDRcUi_B1a)GHy$uv(J=}b8805xlYD%eAztK zkTvYneI1y81DIb8%(-{BI>W)ApK-7c^{Do4m`@nEJNNnpXgdL$JO?(1(Bm#_zRw)G zDeu6Cb(Y}sEB*@>;$0^%Z9TqNeh*>1iFSKan-@g3FJ6<}?EPCU(bwyznRHLUXJ@BZ?Yl-GJ-3wyW3 zC;+1|9mAfDuUYX!{Djzy|E!%(V61bV3w{+}qqE#a z3lyC???+uPc5$v`8 za$-`pPjdEjwI39Hc6Jy&5&p!RrnQ1YpZk#0d#0GMa&UHnr>;5pKY?-Qnl#78i6T>? z$j{EEyq0K_iJ2g}=w9{-ANItef0{wOhl$0n<9)Lyk=r_-TsL0u!;Z#LYzyW2m5g9d zW7l6S>LP0-AHL5%yYig&bbDI5w|iQSjL5?WKLLG`*Rcdz?(D8AhgZs;$gyKjD(4`1 z#MEvu%S;tsE$m*=>X)v;XPsuI9liR>_UNJ~cbVV5a5ymWw}&H(jve0ggUAz`PF%WM zwjn(Kh}NRP^RQ2@xxKHF@1`#*H%nf}iVK2a;rHM??qj>ogy-jan%_guA8s(=_ir=d zeapCO?p|Ot%B(&(k39);r|-GW4!>7#Q^uwY=dhRAlwo}5um^E=-?Vp-0g8o*HdpoU z8~yTooO=^Yn&c}>>?OUxGZzgR#IST49xkKFyq_`{sS`-?=+F#l#AUH;JBQhda5eWpVDf<}Pqz zOu?;r+2fl1qmZFZ26*Ikw6f3Dh5N##Mx?>L6s>*M+Dz*0QliQf7cyW?ZlmBMj0 zvNbp*7;Y@H!`3q2X6JV``&CPQn}xq8u|bw|pN9|L51tDgl>-R5U+_1|GM?1`jo3qV ztbr@o_mtdg%C^I5Bh*nDTR|Tgi|hNEhI*ixi5;G7PqW&NVBe^H$zUfAh_QYMUXL*L zkI%Bn@6VW7w~B5g6GfBvqJMq_JQ{~iYz{8GeRIJ??o?)c1$k!Cu(5n&w?+B?AaYyd zt0Xr+_zMTfP560u>-@c=+jg2jQOlaMIV+>Mr6Iw?$$YSV2L3YF*x~5mSJvtVM{ zBEuf{A=bVHCTo!o89}_oB0s!TIl&IsvoB<0H@02YS7x1UTKwdhs>Gh6oiKVcJOa2E zn6yP($50R6(kHcE5_M!lHsgsrlP=kSi}?Pl%OImHfNOn%Zv zmfpS@+sE0rlw5jcGkIyDlSw{v+KdS%+=^ZI;0NR}fsReY-~1r&_OPbB9y+OpPHfh9 zGw(BHCcUqWGe0eXckg`E0?(qEFPg|DKQX~e8mUuBIhVG!dIr)+fw7M-H|Dt3A)iO* zZJSKZcGhcK`9%2C+o!WnRmeV7Vatl21n0i>{)sh{Y{tymQ)3RE^+wZ6Flt4R~uxUX7Tu(f;0=cGNz+#hN&6-%lI1I z+eo~}RkqjYj+sRtVSjJ&n1yvqpi%lV?Dy8e1Ca+r@1EYsGGfD^A$&>b4AIc|g#mmi zSr#&w^82vYRp*X|K;KKZv(NigYf%q2Y~uoCM5Z}ER0F>(w^wQ3`!C$b<;X_Xt>s>; zVa7~T^Y-PYblgnkog0S@!ku04)Za!ZmQ=Z!H{RbG$0R!j-^o*yO8W0yC(qZ9rYf49N+m3MA^!VjMpWm&`tXDS1I+bZd=7h=>p|>{-dXr-#^x07`x@u9*JJx$ZDJEQ;Jf*n9aBGDSF=vS z_Sk1UJGz2y|b_@yg3aAfW>>>%#yvp;vhCj&)B)e@2|JRzsGOde7@N@ z-7-1c+fcgA@@5x$J#!BHt0}cD_9jevN#txZH6Qp)gN~%<3~O%P2^9HS;#+)auYrb! zaYpz@3H%<;#!dovqPKm_>2_#N^f8kARQ55x!RbncfvlU%@e+K>Jbe5~Z|GVxNmcf_2kTk&-#^;;BPa`l^)kTqCc<}r#{yFjZK4RI#_mTnP^@GSgtvwFq&ZQo_|1A940KfhT zzC#w)8_vqU{TAu8S;m0oQYF8gLK~LWXZ@tsmW2XHm?Z?q!fwL069RuB#JuH)7pqX^O^g0nMqsM8vbvCH)~nPMdv5h^s?u?jIo;XTHggycy=_;b^|ZX z*QFoLdjucRe(KyMm+v=2OEeFSzj$I2?!>`WF~3^urL(_iY940H|119IUV~r552?7y z2X-&QiX?3#N1?`%Bk<7rvt~CE*!5t*$;8m zR|%FIX+PB#=^18LH!)_(5O;jOFPyAbM~+|NF)_ZOCAuTXmG_0_2fHg+XZnv-_VfAz z?03m~7gy3dgZceCJL6N+pgGC%w3L3M=oVL2tG~8n-}ej}^Gl5RzdM|J2m5WT6+FAj z!HX$DPsYB=Z)9uVv(%M6f|`KCdZ)l%?eL2EZwLz&i~i-P1RHJ z|JC}&tdD>0`sSfciS>`c`tr)vw=7p_53;7_F2smNLeV@X@;VoqWx* z@7nPzS;VF4zyHV&cK_j9@infECwnhC2OE&~Q*8}w&(+6ikNq$Q_M^c0?j#Jvn+H?y zun$}Ky=Cm(Y#CLYK{?@LA2#m6Bu?I2X7ldK;^EBKg>wkm=;Gexw^gaQnsoBL`jaeZ z=Dp++c@K#z$tS)jv95U`7GLu{#wNNMPP|_gegFqoz((|P5dHHmduoTEmmbEs=jBnw z@2@i9Rm}0-vIOlML?<0WFZVFkJuhF$dpmps{RtPs!_~k?cy;U!>=N@NYh7?G{Julq z@|_Y}xJ!Nm*MFMzX?(1IVSLlUPvT?y(Cb&wo-0d5n~q*0NR0UMoGNsFPRjg)DZJsr z=f7yrg`aQoXW=7S-k4G*k^)0Nz9GTz7kZyuliFtvT$2_skJLF<7M@&Zi7ub!eZg4b z=lNZR&n16-aQxA2XSQ6Me4h5V$_pD=FTDj^wo>M(XI#r)fydRnPqp=ZkH%|WfEH5e z?L2UC0p$YY`pU3d6odXKYir_G?c=TZHGKHa%;K>*T6=z(IppM;Qsiy;Fu$dzUZSoA z-lZGO#jHoL5ibX4qJz4P@imip@6J0^JaFC(v=L>z!D$H}l<*;YwNKBfFZj!jk#3MJ z(}V0urTN*kFBu@3mu=I79CB&?Bl>mPfQBk4_iD1u!M4%#WE-w+lv?k6-j%0}B~=E# z2Ti{ZO;1++6gl`FwERA_{9j2J4DN5@p#J`wa;Y*%^65+92s}Sx376~6ZsFOk9>#GP zd3=CRs%<3tkZmM>k)7n)NU{yjqV2%%ea*)FOJDH2#7Z|;#nQZ^o1+g`5eq>ae8Z;A zQNEYsLtBq8^$2#C_VM=N#|e6@<}T*N`yGx?oN|ltDdl;sX0;)IaAi8Vm}$#PI|23{ zx{bG4wv*b!7ORO0H~2rb)*r#XtoD@B;K^F##i9Q|AHtidbC=G5ww7GST;*SK?Vuyf zL$c^p@E4AM%URu1;Xrz^*PFfLuUUO%)5i>4_xkYxhq!+MMKM?YjNg9_eJmI{P|tiD zx=P0%Hxqx{7Upp?da@cBRF6My3V73awldarjOl&)KZuRz&f_WmOK+TN9@;-tUA=d0 z_dSE?{UOaWh2Hn1(EH7_oyzOxf7q*JexeV_%GJsFNggeuogadSRGH&PeyIFG=}zEr zU$T51xaXIj8*F` z%jHqp`Ud5$ryMcGj@~;JjIN-}sqp+;+H+tx$WC#2v?5tgGTgOSTs~#rvmfrIl+!#b zQ^w)QA8hGU$)94in}Uy2`Sa!}=xMP08kbUEV;d~PvXk=~NjV>55|4|&wLbUZm+hxt z@H>(I`tisAeE#3qP}nEl3z}fqk6&24uKkGM!cCiv&%9B@SySD!qO+HZ7Z%@(e}BUX z`U_W|(0-wFE`RWTn;)A$!Z|+^nbcBz#^;yQ9)57qJHZpN z8uAOxC8l(48Xxhx{O(zNOLyQO!Ut6F=D(%1tutuu+CbmGoNcCl_+nM;$Va)icy@UT={$mFh^f~*`3A%cwYQR2jU6#e!>e0mDR6!r9_i-k22&@Bk zRqUzNDR(RI^_#pF#V^>vq%b*m&0BMhVviRX`>@}%j`x`{7nroX5|iaAfxd}}$SN_T zhL)H-e7X6<$W@wMV$SAWYa?|%L%X)8wam(^DE`Ksra0Q*S#tOyD=Zk)aZh!yh5erj z+4T7x)3KH4<9NK;9C!Br?}hH*q3)tIaL-;Qdl_Bp(^~xUVNIz_+0Pi=(RM31{&{?j z#_!N5d&76LPoVu`4>3K7c(MdN9?deVgC1LJ^3wvuR)-3q$@k)=F5Z+=^lAJxL1X4h zCmngKb$l!Sq_*|=tJtsN>}tyx#u~!@w4i70i}?U06r?KIFLKT7CCZ zk9p#&el(vwG3|$)m=(N4XD%v>%xM+U;Su&6X`4KXC2C)LRjIT#nZ2TVaCH;7S`Y3P zg1cfXErGMyf$7q{zW+|_6ODghAYAj<`(6En+4v6oG1%yH6=TG>7h3G+#M#U8JqY}q z^+sMt4|J~Bj9a+Rtq1%P%hs`H3j6k~p?*nTh3k!}Nn2`4fBB!fOU?_g=XHn{+kk6` zF^g7=l{GC24V#zo2lDOs?~f$>_koYOH-5#y@r_}8YGW^bchUD3i5bOS3h$-=6?f4F zeMj(1+(-Lw-<58-K!iUSK1Co*%glkM9!CkHMd>ya5xi z$==8JG@IXr+y`}GU0<2%JGe=!*4M)k{gm475McHxa=z$4P6g+;s7KY^zq=|H~9Ap@$av|zaPZEpYP4vF$5ns zd$gQcA6=D)#UGxOnUX$Px0?(lth6Ev9b*Z@$omXUP!x|xRj)iKOZ0e zxeW;)|5v_kN*8k1-(&dC`p&dt+wg1FENgFemHE$&#C!)V)E}YR@phgy?2h`j#9I_ zcMkq-;Cxmecd25tyYzj4cj8ykkMLE9%*Y_`#M)aN8Y&m>YP~0#%zQ6C?NoGg2J|S} z*#|B~KiqjgfKEL4{tdqW!SvzM)?gaR#`iB;7oE5?BAI!J*eF+K9^7HVd%sKkjpU}} zC*`}>=N9+u$SdAAc5LzcXXY2bd(rsf)7$X(gYQGv*|ELfMeeSjN0}PRTuqs4iVyD4 zp5L{_<`wv{qB49I?a0U94eVUHY{1Wd&Jp5gn4A3k(Bwk%IA_;yW}OSn#e@T1L)y$eBnuY2)urh9_0HjO438%wUhK9 zTA1lc@Voku&(z_4ep~!r0q?y1lYz1%A6Y=W#|I2p#DM{y*p3h5r7pkpGJoiBVU&0gXYUo*cO&2SugWbx{EP`7 zVt;kti^$rFvBd|cEA2SorOR2jj*x_~@JUXju3@ z#Gb6=!v0nBXcHc-xSF=EDem2EhtGZ|UUO()d}BVmFyo!L1AoE&wQsSnorJf=Z^1h< z4c&u$C_oRLeGjq=9GzwVFZutY_?kkkHIuR=@YqQlv?Y0bJnsY8O$CqPbHd*~BEo*i z>1NT8@0gkhyd;|KnnhgZiwWDwx1eyd;#s-lW`GXN+7z$ZHR#!bjq#ckTW06y*)lrQ zG}wN-?-%hJbm`8U(53US&#Ta_RVLrjsXlb7jr}KEXmGvPo{!i3lzOjH51qWTo_h63 z`zeC`BwG?*pBqTVZKUe){jAlwXnJRHV`|+|{e4bNsVA7-giTYhf&FjxzcPVad=hYL zGBs-R!^HW5xvs67^)tst*qO8ucHZLH2*X-J%*C}0Zn)Mi{@pfm8qih+IJgY|T7Yw? zcNg}RJ@okF_&=V{dFb)BTeq0u4L|dlny0hUv$tDA<~&XQh~*}|A)99(ViSzvEY#Cx zNVeKiJNFZl@Xn*?z74EXlD7AK&zx#JSp&wiZezT3W76Kj&RX;__7?hNq5mf0LB5Zj zfLwL$L!+2}X#c19t0fC$pD3=w_rpY7hwLvOeE$yiSLo;LrKISi*%@Z_VrXYSyxKD- z5d+cNnp?c**}UR+;ltOaBiFF4e>=s=*WIxn-s?dZXuq?!bsptvD3{2U@#jtPO)5Ja z8^QPOzOsEk=JS{-J*VAllzfo9=tB=k2h@FoGymXQF#(blvI`Ed2hx?a4cb#;yzD!F z=frnO2Y9ngGVa546cP8)l#K6^y&&1(>H^scvKczzG0}uU9=5{AbMgMUc)09|Q{f?D zJEqb`DxGe4{M2>i7n7YWS(aK?>l5s}7wk(DwCVP}V9mgG8dB`m0CXum;?k!27rjTI zsRHQyclgoPmiCpUCuv|a`ZXON>fNSq+8%HE9KmxE_1(U*Rww$pC1E4){0@CRXJv6- zW=KbbzCua+_?&b*{Pb3H)fW1=%r-SYgzj&()F;wOu7`o&}P@tQ#W;R|3mE4&3xOe&(zL2Ujm1$;XYw4qBVo!T07zKBb1T7qqu_t^u6y&<^&H$ zXQhXGZ!o1ZY*UKP-gP8*bPH>#64#c~7-W~o-z*zH3J+_(ny2QhxxN;QPn9l~y^+Po zASX39n=^T`kIu!uKbt#BE@w~P)xVKNC-tvI9~Sb^U4JxfikE6X@?Bt;wUyjluKoWn zDRmgT>YNoBX#W>0#uY$VQx^?LY8 z<{-XS9GA;SSDX&Kl6>Uy&`9-zPsm~~pn^FMFM@|C(}@q#^_jd#Td8HP@Z)o#jBJBz z#EX>u9%WpA%0s+QOTL#+6w4hwzYotWL4JBJ|KB6 zvY%mIU-OyL2Z@b&fY`W$-n0^}lO=-+c_+QTU*l7(aB_T;iJ!P-1UmTw)AF43rR>EU zOwGBJm7OR(x!oGxuP2+qExwX$>DMWo!*}a!_`0cSLdQ0tTMLZ0;T+n~*;wmAd$X$# zUu%s!I@0}rnf|A|FNR08pQ!QOgno6$*be;P9@AHrnS^y8IN~h7V;fJioHf&;eWBKC z%b@X2`D4McKXPHS@ZE<`T(Z0g*luazyc_w{g1?T}Jk0(;H)|4S4b2&f0BdJ#&)ASj z$YRb=bR(;LmR&A+OiWfz!4dDG>mRe1`c7+ID;XA{uQ+Epdj48@VI{Gj8?pbk0<+cZ zC2xmzzC?d|AL5ztm4|O>Unn?!HT%$Cyv9gT2)Q@2~Ni)qAjedyuui zx-LHLAB;7%oP3Tyq|ATveC21MQ8( zIwfzwx>uWWN*xBfZzVvp*lNqF+n}c)KFXvK!CvBexe-Fp5lRS8h_zT(3 zXZi=*&nM+V1F{h3DfaX5Y`0$48M4ntG@LUF-!pb$G{rW)jCG@hP3g*l*M7=>>bw6h z)Bn%0rw<9|@V09Yx^&H2VgNnAMma|YedT07L;fDG`8&@g-=VMPCF4b7`}ub1?*Z`Q z>W5pHm#1IGTP4y%iW8BaD3u0;Ytez=sQ3fdU-W(Yk_=wsjAeAc{NKyDQQ=X3_e1O- ziZ*p0noFNLHxboy_KsYic^!8FT26e_1SdY~Zv6N1pX0*}N3p5C$hcE&jP9`}hONB1 zYwSrM=kxskIqgmZzrtw%xvw?RAHcQbxzi1%4~Sni+vA{ z_JL#B{i>s~lb3NONqC-)z8!?Ktj7l8`99W)sd&!P|3P)!b<=IYEVb;0A0^fz@4^@T zI-aK-;A^12`DUIw@^y|Ko+Mc^$RB-u%6sieJMCfrYJW!d-+K?|74OB@EBo)8 zloh?~#eXY1R5svy57!jS{yUNEyN$Z8&HmoQxy8NDnD89xxpw<|56|P79VWktBd_-3 z>(;s@m6n~@VQ5&gMD#3K*2{P#$H)gv4AM#aA?tq5%d*~cV&;fN$!8pIkLuS+{dVSh z>UF}inxEv@8H`Ev|3l_ncZxBq+!szU?$?qqR~^CV{bvTkXcTn>AHnJUXHLS0Gwq*& z{eN0!wTqiy3Vtd2;aiNW0AH8vg~n0wnhSp&pY|iC`UBG`>*uKi_L{|+h>R+-Z%BY)B~#xQu?TeB16eqS=3am&s+6}*Kj@&DJz zF;!q=qgkfrA@DgQh%aZ!_*;GaF9;Ibi|<13t;^#z^4Y$|Jc++w)Hs{-F8r6jc{|U@ zALr2Zsd)Bg@X^n+>|dqwY>wcVln1A(`>#n{Y9Cr{q}DB`uG?=vPJf5<;X#YHhA_*^1lOf{%_^KyNBZH z#M#iTXhAwrI7{ls*mIi0DR>3^gx7rjyCskC?g0D$1wQtX@Q3f`zhXiZJ91646JL|a zkufM|b&Fy%V_pH;MI~BLWD}f8icbGlh zxz|ED**+{{e=uXQlh-<0Z?)ur@BbkNrK=jhF8*XE7bVY6kOQWcIFvo?@v7V?%Bfvv z4eMDvjkQQ=KMZD(JCJd5=Cv0)c2^@j!}wXBpS;5&k#8fBqjQ~d*N{7@?o!nuS1NkG ziX1Be{8<%g#QpFb8#X+a+-k1>D%x#YetW@*HBQbE!G7n=Sd{oAJ#XW=#$|gJmpQPc z?W>7jnom1NS4@1vPak#~Io`<08t1;-V7jRhEDay!B6#IXW;OfErRta9b|;UR@_dKL zXU*nV`0C^{X>tV9Yxg z#t#a{{C|mZf^i4+4SL*{#(szT1U_XNFZqIPp0)7I&zUr5jmrH5bsloOGv7k^sc?aB zGkZ`u%2gK4B;OLSaq?s-j)t--w~c)_l~LJ;DcACVV#@xLd}H`y9lA6f#z$-n`I1DF zALB#O9iX8MJ2o6&DmiqBs~J;r+li`G%RPC$w>`Rgt?qnziG3E8SxvqSla<>HOk*~F z{1?f8wj8)uDu)Jg?LU7*e4u5fh5o3s#kswW#J6ovoa=rhLOfi3aCD2`E^9HNGAI9N zU6i{H$nmOpCf$J%@z%cS(uMnds_{pXO0vPG~LJ~U?KRg5=fy`lTm#cQ1- zEqE@tYxyYi!3OBLJHu@JXuV;-V{}Ivw4!$|Z1g>Il+}F0Qh#3~G^8BL6~^0JkKF6@ zW^_c0td{+gbk>@Cdvqp&{}E5co8)jQ(eLBDDtH)F5OSq?aE2yj*)$b z)m=PK9i#X~WBfDk$U9Odx>nrP-|s(}J4o{veD^bN;F2R4?|~*N&og1!K9!?PI0N}y znQt%gY-!5z6T9Xo)~zkyENSIFZx8&p7g;k4-&h>q-2w8kM}Zshgv0`{=kOr==;TwV zeUd#;eWN!Tv#>d6&rgnFwc}x}Nu2Op54px_e?~d(`I!wJ)kfGqplt0VVo~+I(JX2s zSC#6jy!hO$U&S4`q7lix5bp!z%QTEX#P|*K+JbBdolPtYcDVM9$=P2cUm4@vC7d|oGiB)9fzI>4YR}6mR4kULdU#|6my#PnEgp3=TdyYBQz1&lxAJE{ybpyclNM>)fS$0jWMOljc2&$BNVjA z>5$ps=Gojz``ehm@~64}h^(_s&AqIbx6%I)Oxt^tdx&RPmg5_Jhdsm{=xQJFGL^t{ zFLdYDUBG!R#dMwX70w~j_NBD#`Zn*w*0gC8UZ;-fucQ6Mt9ZCo{Kg@_)guvW@C2?|9izB(7+bx!=(ek(4~Xu`|*wDn)!VpzB|3? z9n!sfTCMi|t32&L`zh-S;I^l=lK)lhsuLhiO7!vs_!DkyXwjiV&Q86`bLe~52Iw^_ z-Lz=D-=y3Sa=rTTH&;NfuT3$IuOS<_-Vcly0#o@MH23>~bKDGVcn~==jJC<$nmv>@ z8{N9lO56--VBfDMlQu2tM3en*fJS}PA#S-PlV{>-!PSS)+5InHLnr>rf=A%TD0Uz9 z3u#B=R1A@PAW`qo4tGo%3;Q-TnY3%s?m=*NfVL~}gS2B4#%ZVSf8sTl;ww{L+e5Tr zC+We(!+zyhp>7lJSSLN=|Ht?i|22Wd0fL~d4gO{y@wm0;!H1F)d8pnsG8sdq~`GGQI7Wjd;oqRRI+skSAlfhSry97kzqIKz3 z@l_|XM>0R^qn%NWpfonDT7vo8q#qsZAFmDzdP?F?C(SO$;WXIxb@?c9STj{ z1CHynyxCQjnNyE!tc9N%pbPO@Jvv7@g>Bj{pzX_$^UF;}!*kG{a4-4i;%osj%H_8M zDf(suGR=R*XZ4Nz=jS`H!v8=YZab5S3lZ)mFN_(R;1lK`*`fO@e#e|#I$eNX+{QcU zP0@BqQfJm$rr|8+aCwrp=g?kdQm)^Jef0qIf54&bp$)^C(_q@Z3|yqr_G_Z`LA1@9 z|CF>nf#(%T97dl9hm22aBWcx8ctDe(1dd|3Kwo*8Y;k$$1M#9%RVh zz_Hqt&(MBNerA?S#IrAgpoS0NkMVULOadkt9uAD6Ea+>ECLo+#Hl!DkiaT=)duN|`HT)qc*acEUbLjCX|tpBm~o{U6bq za!AJyfq|c~)$uL4A-VD~zm;3*Ch(?r)A)AB-$|^J&Wv}Vi#sS+#rsG3pSosB{q3$V zT_26|-y7x_#OKhcb2RuIl*@VkrJP|Tm$N_VbExE;i6LkAa&Q}fb{QA*$L}!G)Ba2e zIaHBz5ppBR_ux&#pTW9H`$dI(Eaq?lcGY~39j-R{IqSUkH00n5T4U60Q~V=)xUBua zQ!MaRa`axLy9xTH1?FPEo^8j1tiM%P?NwQe%T_SX!G4!6;)jsYQw=ofLxyUd&DyM_ zf1OQ#LAMY3EKr}Ub9;bgfb~cPW9}{@|0&~_zvGOvm{XA>ubpz5kKXV6622RrD=w)1 z400`giC6{W-GT1fp*aPhEsaxi`3q%i@qW_Zp>=}#5sc_Rtnq1_?ihX4vw2=kndd2M zQhXh>nKOwyNz;hwUq?ClJOqR zSbOC78XNzTp1ZK<2EWX05qlucdii1cQM{WAYvmqxY0UKty7XnBt;AiU7UMZdV>L+{ zTb<(95{>!DGueud#b)i;Ph;@&2*wcMyp>>=$Jp9DI&Z_hw7>%Wpz*!nV=WrkmcZ@J z;DI&t+@AA48QVv|BlyT{xhI#6|hV0$L=bTuh^=B$ga2{gweD20@>4Ev?{Dw0B zx455h3fx~m2=}A@3GPJ$k}o~bo_rJDi0(!(i?nv}UWq&*_GCM`tD~7#dl$UqKc79a z%u0R}4|_9wu6&uHGg^@OI|_b1>4Oc_dzw1R3nv~SCvL|-u(dqkRrkhvCv$HYxlY~o z{!(=sSXcOY|K@ANAAs!=Cn@3KRZ%F*0WYxfCmq8A(5`XuZpuR>JuU&Zy;fY#<-| zApGteEDH-I3zIx7eo4q0a=JRQCgHmdq|mM6wo>1v(rhR6luENv#wc0v5U|vE9)vCr zP$rdTm6I`wKG6QW;(e+qBmJOwpBvLq&Z6AJCHvKv|9#=Lkps&x9O$qw^ zwc{Ta-k8VvLH5+Ri@C$k9WIR-(|@V>R>_PgxSS8{B&QlVA0fRcSp{sk!`N_lGIh|6 ze$J>l@(S70k57D@-QcrxGWZ0SuH2G*5I*aOw-W3w;S4mmbo!H=Zqhl7Qiu;)Efz0nq%=zYC9DNy0^?q_T%E#f5wTigJt{8N%-B8#AiP6lUy@NJvvwx ziZ(QU^+y@UkFU85O__`Nba|Qlol$7H8~kr4mv1+?-;%(6iE>uDH0)#Eb-;O@C$WF> zdAL7v__MgbWDxET(9b`^y(^zwzSS7CuR(kUHb!Ni@LKBJF`no6hV!pIv8FD=j%^_y zZ9R6PeEt_)b7Bqa;tu5gM!}o25$%FSsvb|3(JPXB8H%4B9VBNSbv|VNt~?q1JH&Z{+y0OzuIj$c2xLEHlpjU7_)uR(mXc(WJ*JFO*G%{aaFT z%v!Vn7&VGlbXP>#pt}~Z^^Px_)lU0Pu6_qbe4SnvR&+Do4v`On_?_gIaV^g310DkIM zIs^FUC{J#*iFG^pj^2hJw2Ad?wzpmQB=#{3Om}uRS>*-68OOE%6y3)X%pwB~bc4I@~Z_prv$T_r0A!CCLR(Ct~{68Fn+FX0Q9CFhyyV~cvt z9PX*zEP6_Xx$58de_2;>I=*a6dnL3Z8%J`=vO>3tF1Kytt|ik~c6`}ZXW!5*qxUxN zf5m%WJU(r}bH&rB-ii3M)Uj7nUj9M(Zc}mN_Fu&u?inB4rTzso;lhR6M(kjXLGw(V zx7!Y9no8<?6M2ydl~PCgsaz@c;2;iTiI37CC-W@kGoLp0xhS2p-;CeY%&j7VPaE9SaVJha{>MqYOXs+24xtPW`=uF46l=`t{QbNt= z$9SU)0*gcO@0L09WlgFv@q6y8X6TOXJ? z$ev!o-K_tpoo@I<7Y7#mj(@jIv5#(Fr}~aJdAUC*ai`d)w+(OC{V~BLE$z9NyMZ3| zw)Y@Mbl=jECQE+z{(HxcoXb7JHSh^{Dt~m{(Dsj!^U5*vrss6$u8pJh+3jyF{cv%7 zxoO{9Z?))-3*CXl9WdD66VSn<9Utx8&`=?BOXmz{0kZcG*z(HHQkZ2gspUQagWd^{ zlYR;`%smJnXg}Sdag=Z_USzPw|tZN%xjve_;>K=6;JS+JTQF^I{EOX?NQVf%84ItNFhTd}9s6+RM)~ zEf*3?KApSNqTGFMYeP#KXk!$lS%&k$4TfB@W$i)}VdUR?Y=fqGJMS_7dH%&Q+&{ z>SFGkGM?US-Yqen=KM_ddGWbw92(C(j3sm|J}nh)nv)AV?WJn1)J0j%Q}uc-!r!5K zR$roCGEPLWR<3#iCfk!x0-U?_r`u}a4uzY zz*iWW&+)*A9&oEO-zxh6<(#_7{@pQmQrE#Zc!&4GDknM=-3ce1jfwabkCl+U?%Gm5 zI@zY91KM*N-Eqe{d}+j@_=r8J!w=%v{>Za{$HZ#5`#jYa-Qw{$wyf@fyI!;f&EHNA zAJr)cL@rtG30{&~-p$FRz6a!F^5!@>nYbUiV+H)CJG+iyS1OL<2y1fLE!`EKnDoTk z@Vaun%TCtbYV{DaIxxyi`}k^4>|=ZxAIWXYRqW%d zt71`)1J}9zaG?a6Eu1HLlQBe(Wll3i zX){a(YpDT@Nb;BDg8HIGW#@MW86 z(HE)|R@%SLej=y{@*omj-%ua?&7YY6`xG zZqPYa|bQ% zmH&!QC7bmWeQ%j$!oV{IKEij?ZWMScUwVN0NgU_g%YVV^HGT73YeT&sL<>20@f<&A zIPzY6s@{oSL@ROX>TKY0WVXYf&|LTSAuTTaHKsYh-5C=;Wx7_LZJLg={0e54;J9C5bfx%l{YX-`C z!T+OY8JpwNPTYe=UyiKzigub&#vEC4rsc?zemPQYTFwWLfz#kWaA0xYp`ZIbE4WVq+;Dz+Jo092he5C5|6UW} z{xp^QaEcW>Qf9?IElO|EyWCLibnAJ^1QVNr?Aa>&3!X19;~MbOI6N=j*Lp5WpEKw) zIE=P#gy*pbE&0If6a3E}P0gF|>Q?c8S!Gzf%38c8BL2s(*^SN6)e3x(0~ijg?V){T z?l^Vs<#xu2{SuR$X~YLCdq;A?KpUZv3LLnpK!1rF7~z&@HetpXk0-Bh(C zNM9DRMsXR-DPQ+7dK-H24Kpp~$PXSB-N>HqgtjDCF4J%60_aEc^Jx7pe?7WibFHO( z02^u#@6w<%_Pt_HAvY?awJ5YJ7+$!A*ivZrF=#A6|ALL+A{ZESu-43c@3Q#BGq{^A zST)j_D7iZ za(u&rbCE5I%}vO=%$WmhTZ^;J@#@LQv6kL%Oh`|7!0bV~4& zz6N}0*99+$y-Z#t=s_@&4Dch1qc<{7XxL^=7kw%58~dTAyB_++ev8tc{C4%w^UD`Q zOX$e}_h6^K+r;w#v>H7Ne$OzQY{ftVLj!yS)9A}~OfW_^IctL>HP~+rf^U9rg#F@w z=zkL4ZTxp(Egh@6+Ltrn%YZMzd_se7^Al=U`wI=_n}4MDo2{!fr%#(&uXW2B?*CB# zy2Gsv8FxSBL`&rQ1|JhUih{vimm(7i(1**s!L`@(KA6P6`cWTRp9V%?p97Ch#$5G} zyLQj!?(c8&EXuQY;O$!E3G=OyjTAsuRI$$OWZWwIW!)3b^E#dzt$!&mTZ1*vM$N5j zf^>k@><`xV)gf29K8|m!L}qNF-=<7_LHH1Fz;{T#gN^&QV=Iv};t!IsTge^KWVIM_ zL>waLd)6Dl@!|)?L5ze(>!>4{1>S0&vdog@)a%KzKM5k&L!Iv|W# zU2bWAf9TK(%W7b)qj-vzDxYcDLV3wX#T95hd=vgg>(0A&6IVk0y3LiGZItf{S$Udt zage^!e}mtXzMlRlzEOLC4Igplz{+kohdWAdN;g;Cco+VdubkD~JoBvPSIG6+;~i4+ z^jC_Beu^M~^!U(=DJ866*8=j9wqX2;R`G-RLQzD27f z){x#Fg9krB27fxm#6CstK1f~ybi&5YixfMO+abBI2ihi|J@Rp!XuRYjY(3c$y8Ck_ ze9s;Ma&t_F=PG2t6zG@r_VHzv+(lc34yeY)nSpHL9^vf?f1|@kDQnLF`~Yp_s2%Z@ z;;=qDkHj3*=SR#bF%OIL{T}Ql&BMz)(mH;|*@Pf_d)hbhL9=RGV@d4OoO(Q8W6lxc z(WMvmF=xrZJz6g_U(U)fXZbf+U*O-rjQOhVXqg>*m3segZ4aI+`Zp)Un6P(C;LGrT zGgr}-`-wLV#NQF-BpfF2r+bz;BjDiB0}j0%-($W3=Bqu4f%CP{3n%AGyN)fK$C#r7 z<|_Q=Ggs+9a#Az*D&Q8y7c!i=s@)OAv5#rSsIQD_o0It0oW3Ixt?Bhz=0kbk3JD?AkXifbIbbX%YPmIF8k^$0uR{^MaWv4vsb-EIfOb6DUJgR zogM3qr?+=`a-UsCoLva}E(=|f1zj8Lw-2*3+vj1299ZvZzhmKo&C!g%mPO~G^QN$0 z!kR0g|4h5r_rTm9*P}8Gm;oK8z zda++Lwm;EMfBS!$*}i{XP_ag(=(-Qo&-LT&Pd+A3Ziq8o-Rl|ic5uLXrv&fk!3$Z= z8qRI|z(v@o+wO;2#`_jvF2 zqZn+rADuPqBF2Fj;MfA%S6iaz-du8X97aFm6I$-!?lb(*Y2)lqzQI}Gu7|7IJ?xd; zM=YY^W*ol5j$vLoH!k#T=DvZj;Byb}Qu}iISD&IQ&i1ZRKk85YE_K^V%4fAFAE4+& zZ4xg$HqPFXb2bMa_%Pek-iz(NkbRF{=<`5!Rr@8Bao4!+I>7an#l7b`e4Yn==Fx|E zO?e#3q=5+3mKg%qN483w`khNEBHfLk7 zr}ZpQh`j~<__4pSm%@_Qe&#+0x$tTBr7`izv5}X?8nw5HAENu6nFH36ZAHi!@4273 zTlx61L{4tWiQs6nDKr!svhDLD@Ru1)PU8{SoW9W93f7B3{CAu1&jp#wkHJ&4$i$vu z|NoQiUu^klhFu;-H$6SnF2@$w`P2yZc6?)7s)E5?m6Q#D8(Ve0X2RmjW7|g{Q^qIO zIq+o<_NmrF;MG~RtE-I@eT&Yw-=pH`ob{sf>?xL&Ddc+tENWIHI zrh;Evi!`oiZtJy|svQq8w#Wz3p=hugTI=Eb=1;$j4~Q}C-(JwNGWccIklv10(3e|} z)J|WXHR=PyzDWAcg6W0$q5OBI&0F!>{OOIa)lK(A3Z~B|F5Ze3Os^z1R582t?6WJ+ zXZk-1ru%&>$Zh4>QDnT$+*|FO8&!LS@Ok|s_pZhM-RTdFZFy)Jv=04>)@KS{I)~2~ z-x)#v65{!t{Ui9;2Oo=X9TIpu+ zqzs4?Pb@l_!Sfiu=_BD^L8tv7bv$1tCLbS=a^bl+w^_@&`&-m=#-?@;(vCA8aFIIh zPN%OlbW(ap4|5z*PHZ5w1s~C7LcTo?JmO8J-~Nw7gPb#QY*d~*_FsH^2Jc6+K1j#k zHI_Ajvp?v_UBz4MnP6e7kCQ#UqdePw)Pwx(dWLm5bMtvK@PnuIZt!!bX?=eCN;7W9 zk4?r^FAcSqtTY*Kv___}NBVpYx^Dbi*kmDi+n)5uG*>q{XP1F*A+fp4dG68rYufwe z=raTNg6k$}@9yiYAFgIR-(>BO;f(*od1Km-W<;)a)-1P;$8Vfj^5Qms^UEXbCBH^y z9{n=DUi6aI;r}_7wLl)ZNRVZX?9bJCtR476KR86b7t!WX{EAQVyb)O>U!Lt9*L(+i zdr|bF+R=T^K6sLHSJgI@_w_tLJP&vO$%hgz6HOua&{a5Mox5En|GJB0`%DB+8Uwjb z@NM2k&Yh%haK;@$;5s_NYPUwdXk!aanmfMzC%0zs=> z18;4!lc1=0BVcW7&&h;L7WA3gYO0N9RHTiZyVUq(THx`lXG;**;NGG`OES3 zfX6@llihw3=R%*RbNW$=4CZG(AD;0I^xtpdd;WI4c+}}K;Bpq_G#6CHP}VK?Rm#Qb zNN3G-($PL-qzE}{u%XJ%HpZsE&De58OQNAAywmf2@Uswh((mBg5FPD<#`Z#A;ghlP zCm8D}YfljSz&^f3-e^-UwVC>~Upz`NQO&UaI{ zGcF04*Y$@NP?vNFhv&fWPQY*M2V~=p>#*@X`E2ISD}-$oSwk^@htc!?;*L|&I55{66(4Yz-}iYRS@L-wgR5IOKC|Wh9Q^_tS8iQP zx`THgyv^;S*0X0A1FdKI*l(lA+5_psK-&BUw6XSZIzBhqlJLP{UkKlRAM=>KSkoLk zS3?Bgcvfrv2I5CPU1Nm6>EMseH2ZI9_pS=-`NgL?1erQBio9t zW(fTeEr>o8TbvS#>)oy_KLQ!ip$on>^tTil`I(mVoqLg0q;s~gKJk6=6y7O5zmR9^OToAL8AcAHoS!oaR}dVB$`MDd};X=UK&XywGBE7-%6(0}OX>Au@TstbU_ z?e`e^y@fHZTb@(~-9}}U@3U@E`p$0pElc0o6=zv|t2_c4zuz-v0m^N@eZs~}FMDO^ z%SYFX-upNFk#P(yv!>U+Wi2q!-BIXnhuWlW;Cj^=6LX%^=DFHRr7ii{>L_o5=T&Q* zJ-zJuWqnM&dCzXg(%rs-_puAiLUhQP_p=8jZ?I{U@9fyaX*2Y^0)5E+Kgj>>yc4b2 z$n*w!5Ar*Z-{*LqO?&S83QUtn7*AC<)}a@NENlAk*%ieN?2}BhUpyq=U^9H@H^|`M z>lrG(<7*8ItB`-c|D(o*X`#rW-6uJJ5Bigw9RjWxznpn8^SS|M(xdi8RmyQ;E)3U% z@eOYxe{yf$DK4gaj+s>CGn0`I6-!pVn>E9RHz=NUIq>X3Ru4XI;(1c{lQ)BPHCSn; zXWnZqm?s{B%q1T{aOulyt~O>e^P}3FoU%(;N*2j+>M!xWEbmNu+0C(CxR?)S6N z4TA1Hdq#skGtGI9e&E7j;S<?`Xb`-&>`Dw*76dQ+QuFa2X=JHN$fqFrdUk% z=}!7x#538Al6ih1_)5s7PP}S9b$A#T;WG?AZNB+{Z%XKUp}*kx7Uskiw4Ku3j90#q zHt?kVv<2X2w((W%<6i58@&}Sz&&fqn=Nq~Z*k@)M?*be6y0))!f+0e_H`gASX<2Q* zVx5m5pR5}&WX46N=Z;!@F6)fRHCFc>Ppo@%=sM)vO~Cbi@P18qa+!y7+b-e%5mri7 z6kExK1MEo`o{UY%N__jo+GRt_8Hc^}r;c@e89G=u>evlE+)ivmvo|HTn`P}NG2PlY z6L#h`u~(=5zL11$j>_MhE+Q8-F=~eJ98wrO3J=t37XaFZdv_=ON<#a*Wwg z3%&k^cBLaAi_&N9uiElrGW=?H3hhowv{~*W9*jPyU1$Fja=*Ke0>1|KYI5;ZqDRq| z@!9ov5!+xBOIU`#Zx%khQra%X|2f52PVC@3>{4^K2NwsB^Zk*M#W|sp#p{suv+GJQ zdxgKPvWhD-$o^$%#0&D>BOcF^%5I(|%ez^Pp)8N$oZ3c%tBOZWM=9<2jj584)z6s3 zW7^_<(wgJRc68U`jf`j4F-pvfGrn>8QP*Gbr*7n9%HnsKmmD~z+x!ChvJE;5FgLoH zZdHa~#gXY(&vpZ+WLYoY=(}=aVExR!IhEG**&)s`fj4ZLYZhF=z5ZW*+47-Nc&=y5 zE?YLV#CYe|LGOlsX^i~Tk!j-j`gNZSquR*Ezm*ePz7oGIwt8em59?KpG|%pU@Kg2rWbNwvfV5oAHnIiReu@&O@9IwGvtQGaT=uNoY5G(Q>Fxt;IP0mXa0z*sU$G+a2 zn(o-3!aag{jOnW;d|q|O=yYr^$m*A-cMSC)-?9ImasMRXKd<_5|MZR{gVQ_8FNp64 z#kaIRVk(;=W&SD)dEdgGaTvHyj7vw}HIsD?D!E*p*rY**vz+0Hk{2ygS-kZUV&d7C z8p*aNN7&o28T-gF-f2EYF0d!r+V6nf0ljf?D0cTmY>Jbe{EC$EF~3BQ70ji=`46_E zGc@QMi}D3?OBYwLM*x|RoQUk3K#pPlaTb{w;Afs|E~;KTQSZ1^*+?aaCGxkS3tY~5 z6PfeJO%e|;I3FFQ|BW4rgFQ0%#*U8BH+CGq^u~@8<8JKeE?6luDN&~Dd-z9_i}#k) z8y%dx`-LhPZ;ct_ROOyE>dNzKFS)Ld8Fv@=8Xt{~aJ(=<3+M6+Il5aShI$xufQ_Re zvZyeg`|&}3v3UmJlZyR}VuxrRoHFVteuj?w3p?PE>|yZmt8?$Yxi<4gu>gvD_!PbK zFf!0!o~4G+nebIb*uzGDGh{bDn%S-1?rTrR?}yJ7$^DMu8FpVY+2EJ;<9|hem@oTT zGjJQ_T)zV ziMizGZt(rHbe_tgs^^Y@XV&o>?0=bySSP6KDeTiBXf4wWZLyUnm~S|4Y^qf4~?t z2JZOgF6p=-awi=Cer&A^Y`)7^Of)%dvN(%J<)hdf-LY&Yc3tD=?%7z&w~Ael(3h?@ zhh#l+9Ck@RFlPd@a26DvE-?$fy){&$7-@}{@T>94VXQP>S9ck&-q;|M$Ey?{P9*cn zdeMQtuOz-u-|O4`&`P)P$f|uNZJO%c3mr{x>%%`JdxL1;5_o%Zoe^?Bc2}|)*Xnghi=|;;=jAl4YG`4qgJfts#9*!$S~}^L#vHFw0g_X zQ1LZ8*h>ZN6?_<*V1ow(ttx)64msm-XdWIs0Y0$AZi`Lm53kf%x%1T>vkG^-z!P!y z6Wu-su@mlx1`6o+7W%Y#e`=o5zM=iG384gD8+gt<86QmCKQElo-;vI*mHUi-6JyT}^GfNXeHyo82UXAOf7ftPcA zTfmxi)^CHus%K@g2jSOWVv8$^+u{_*6Wy9oeFUEFwxu|qNi%Ksr+ITHey5;*4?KPE zrKa(u*O$AGx#jTm5pkX_9wxUu_X?^|<_us~cBl)<*n|^SlVo$w_ z1vsxkei`(=j$Qcg(C<1nqsLp)WBE?<)JHdx4}*Dr^cGXU2RhsfjU7Wr+{gNfjSPM| z41S7j9UfbZ4YkzWw>aX={Wfcv^Rc3SpY!=A^DE%pSrY4e@O#)S@<(8Pb}7= zIwuBI^UN^c_A;i=-z#69V!`5jKM&9F;_I>+_P{sDpN}rvb5x(HP7eK!JUXh!;H7Vv}rT;Xg8-)WyE{es7`516@nTDSGZZRAAP)S#BcM4EzqvJsYgY zut&PEe#EzeQ}7Dbqqj`KpF%9*e-mcK6lP*y_(J)kD87el5bKd-!HDDzXwjwF=DyuW zwGt#);(CXAUt@;^PN4^AC#O>vV4_S?}BiCY!RNP8*9JXj;v5u7&Y7fxl z_T_~g!GY56hBv@#i{+29k^RyJK0dS#8*;7J)8=IzxN3kIAKr#+fiIo-rTNQP^R{9) zC>wRvL}(Y;89&PXtoP-_eIRdq2(FLFZjT*XXMsnlA6h6r>CB5E#10NZHX4eZdmw#5 zE*dR4rpG9q(H48qG={0$KisVMkaM7fb>(XOp768|&*J;qn|W@d*9un205-a<=9AW` z$Y^M>|J9?+fY8tgb?(FFEZ&VgN8aNBxyNp^>gA_UY+dwqZ~Z6GrM|m^H7G*tS@iXy zdgVe3NpHh{tatCi+gthO=g29u@elb;U2!e>q@Tjiyp(5>1MFdDLTKIt8|60Kn&@5;Hp=H@Yw;LG``@o7(6d9z)}D# zWtrhT^(}h8ja}BtJ$eB#a+GbwC-+ri&J6Rr6~9~N$-W2Yf~)4fSC49E{;BWCVaRFc z;hT~Dy+1rfF*rjTz6PDEe^0Omwe~>XQcQlL&mSpoD=-XXEwb7FD_ln<0}Mw$=lcs1 zZK}QfS$q!-s6Dl-w%m5Rw&|zM(|l~|nS5+B^~8$e`vzS(zw4d?(U{`f+&*N7LQPdC zIZJ@t!J^MhWQ~yQ#^5P}oc9gJ>-Wq(!Mq=O_yk@oT| zISzVS0v#F21-;A!(ZR@}Wd((Obkur!35m!geJfz<*;j z+wRE6Z_!Fj)cKT69#hd|lrfDm7etfDn$BF%m_EpO-a>itu|3Qu=5xn>@D`%}J)aBQB1Ir}-7tG5~=AY)x z4lO``KLSpDAEay{ZL2-;6s@rpACXglGAeuGgV+Wa4*OWlp~+``lk)VP$~3mc;`G`U z8}IUL;cXl^6RtG>H73H9eApVt&ZUW!A&)sLjA3S?7zs0ie z`}q~T!oB+L!dF6>?87(YT}GKj3p?#VpO+iXlxhA*{+`y+(81pz(c_^pP$Q_-bH81}LD7)4&Uju36{ z8G((NG21&8p4s=BQ4#pd8M0%!CHq1{S`y6P|G$9a_`Z&0nf5e(kw;F;jjYMO1`-ai z9;f8)D*{&uJn!|Mmz&tHkjs7r$&jbzUBMdz?mxp%T)OxNVD5nC&x-4<|2?@^wk_!y z+2s8&T;;^-0v9<)tOm&%0kf>8&@8Xf+{hsgtB9)>--ONDA^AF+e5G5F>9Wap&A1Nu)(NK&KXwua_XEy9obx}Gl)G&^{quO`+6vRc?s8NnP0!_FE6}x zSJ->&Pnhoq-EUQ{6kWqly@gyQN%bbnz`|KPj)hzW&R%3s?hs`CA;d#C`;n1v$9soV z#qD#wcCtq(H;SAQeY2orANfS0KQHKr4oj(zZsM2mif-k%FTcO!mo>tbHTIx~Iev}u z_!&3RvZBU!`Z}>p4ZD$7*^pK**olpG_UuMRz)L$GW#X9cj{B-%Pe9A zn}_kuI8Sx+W4-H3dH3mKedS~L8H)t`lLNn(H2kj1Ykk7 z_^Dz1;n{5#HktOmcH>aKy@_^@_e2)Q4s!gfo@$$Cg*^KfR|Gg!kM?6JM*7e9z+bg5 z9a@A27GPH*hP_}3d#$G_k4Q7+v+&8~_cRB;O*~gl_77_+@HeWCY+^Vkd#xSi#@D90 z%KVizOU!vys{5Tpy`|7wNOCECsDlO-pQ=3Re%6-=xeI)01rJ2nbK;KAF=A8wz^gHQ zQga4a92u}@!Ki3v0kk{>JC-R9$bV^w4ZfWC8T{GU@5gK13K1XOA^ES!Q!N=Hn{~~i zt!3C46(>F#8xwx{T=fed@qz=~Po|7)zAZC+=*g0ytlU&^ueP7s;;%}^#Vl~4zKh?z zJr~@8vv(aI9(s6E?2-EE^bwiLqCcDISER4jZhQg!@*Onf_}$|B%FEB(S6+&JG3M>{ zg?52RHu}KTqjNu?U*@eP*<%&<32+&OKJ`41co(7XL;f0b z%ByTUbLd{m$;Kd>DrHOI_iwTGhVf4bk993EXO2eqq~;A)T?u^M2fmzn#`(mWXGeMG z*gBGMe5cCOuD`q4ps|jwVt)tlX{VW*Ub~!FH3an0Vr% zrh&Ka=z+-bA<-oD;p27szxiYAugIh7zsV7ebf8H2f`^KZj%E&$E@c&J2h`Qvv2y7Vn*Rvg?6|5|g%|aK(>?Mq9kq!ucf$ z+Ij-oY5~^AX`^fZPtzFq2fp62vzUuPcV8%UCYyt6GuQgD z9KTlu7ybXF%zu7$z#d+0Lc^;IZziU8+h?mo#7SODJ-+Q(Rga_gd|& zeHGO0f!`Kr-vwp&kx#Z6y3^UlEyKL^L#U@2ns24QEyIfHbI)j`tHw+vVm2Zyr4zoTC;upJ@&PQwdGxu zcQ0sJ>sk!kc3toGyy1-faK>JF{UVocS&}*L@{Pf?^t|w7Gcn>Blp6&O!`jG|;b%?s z2Uru~hjjzY`UCuSEb>;%o)Ni)_HMx*2cL|@YKu$Y{W`BQ0s~i>Z(nt5nUC0)Q1RXude-m14cP`*_r5T#-tWa0c$>4nE%=C> z7Oe5eZj!Z(cyePoYsZ}<&CJc%3PRmDuMnH0$=O^J9Bw9_gm;u*hMf(Y<)`@jqVT@zb zA>$U8;Dc_)ZXNS@ZZBXDNd!93ynxPcuV5dy*$Q2e)568oUfTW^_}UBnVdzqJNDEmN z8eOuPn2OD;>t?v=pKl&MWMR|of!#6Ai$>-F=j2d!T3)_#>_N93=#@H$@%>ETrd_}G z;BYV6ihPwmlUL4d_j1}4kJ5c}c46a4*|QaYge^5v1ixWD6QB4Ly;U@6b5_4?3Wc1B}UW>L1QKKkq*E4$i%aU(K87ZGG#bj5+Y#a6C1=KHB1A&$G9BGV?+- zkZ(d?%U^1}lAo?$?<>vw2Go;Vy}EhdkorRED{$!pooX#QM$0O@F}kRrF<&|<^SBv! zqp$1UGe5w6l>V}>A-4m(ZElH;A17Fu+oHA7<=<~Q@NCce59g5|{n=^t((_z?=7+~H z&hvBsg#HS@RJk7)#LFoTK8*h%3a)s^-qpG$_++DK@gfVE&oXUm|AYNpQ{yt9^NzBj z$xYxz<0@Vw+n?|!8mRkdU*121Q_Zy_yW=)S$%DdOIluRlYu@EoD&K*eFgexq3%2c> zDA`QoFF9Im_0v7L7CyzZ)Ymp*F~p}z<(q`RXR;PL^b8%A(#JaBXaY9JW&sQ)8gt~p zZqy~5oTdlr6+J}3`++q*>#cC0$_GsxSTn8Oq?++>Lg&de6~+$D*cShIU+4L%f?v3VfS-UguBJgJ0N1mo6-nE%$ zTI*NQu0igIt`;xpIo6zDJscq(8sBdenPD37Jq{KPyl_U@9%qzw=l<6w!k_84Z2Ir> zoyH}?d~o<1aUX8EaViH6{7vz3eUiF`Bdu3Q{xu$762@ks@*@*vBk#q_K0?_j--5UG z@CovUnA{N0{tDdsK4S45@9p2g3;DgDcfrJdp60IW1N;y@;k^p35LcP$)Dvd?6Y})d z&sL}1iA|re{~|tg8@iywhdzppU)mIN_|UVotM6R?-wHm655#}ni}M7$=)k@4-S)wE z+n0S*{pyFaZaK2%YS#1qX`>>e9XZq4hZ;o&-1`_ZAhPGO{=yHu3%T)kLaFgN-kUY%_-OWdGp_2#F>ICX=+sBCL!DgYscu1! z);iyUO*V`5IFij?dhj4$Sx0|Q^+~>K$5(O!p4VE2Eoi>M|A>EOIlpDl8a58uL*`2E z3`Lz!@Dfv(+4*kZ}IScV8f@Gns|0Bll1aU;jl1))vd$uSH)9iDL= zw0}Lce*?7N88;hAy=oNu+ZPx+RP2Axe&95H{(J3EmqyuXJpN|-Kg;7=auU1B?f8}U zE9cv;E9yme-}^8&^Y#z#pE#IvT#s$(i(htdgAJaV!PE3~bZzis_cjwXmS(9nqlDNY z=zgXT{i1|*ypHp{d}&*TR)##oifQL0i6cx+dbGZX%YrxSiueXqSn%!Q5@IhQD$YNN}?$FRA`G=4lD-L}YX6dtuN z%Aw~3T?C*3(LoeC5Pb-C(ZgKM`O|*;DE>0dsn+H4ZOUJ79GcX3;=M0(E}Z(|wzr@5 znrLr7?Y-C?`h)-5$WI2@!L|1_!yB@QzY#p&7rk|%QMI4eM*B%P$`_6__mXgATklO& zpX=H)Ct~X-m-LPG_p837GQng2o>$QYe{WDGy4+LkPmJ9}aur6s*VMCj=^En?Ut>7) z&%$0ENCPjd6&lwnzL|Z~4~wVVv|*ud>2qr;PnPbg^!a-FjBlFuwALMtZE)nEq;Yzh zZ<71H1zU~!eO;I5vw%a-r*?VXyvA;H@nm;-wA-^7LUmp8i z@dqjA&OqWSI%A*2hvf@Gj&71Y@*j}PWslbX|M4~K2-sYY-f~6#v2mlMd#7`?T%=M~O3_0Bh&>dm0|-lN}r{j=46 z$FFDDEAOcm{sXLC`6jbHKa{xC#;`aq>Zr=?IYD&VX<1kyQcFFKfyN*!hCU%?_V%_=j1a#XH6H#`jab^P+rQ zdma0iS)p+XgmS9)uJYFJnPwV4djef{d9k}p!S2#Z4t#WECujH%=ay}hys?rq(xS*ztxp!!`weR_ zb^dT}_D1CaSKNbaFn`SWZ1ubRAL5%u%wNSGOGj0?!Zb5|5qsD*Cc;HC@eBWee@ilR z^H=T0JKz(lE3CRGqxUKOIgf{DdM|rk9RF|CTM2w>4CG6jmZ(Q{xcks#uh;)aGR{1& z<}}SD@~=w&QqDMc{Q5I~?4@46;o+sbG*%3XGj{iS1~_{jmNISz-86XneeHhsNma7P zd>rL8x74qZ30U4mrludKW3=5f`ZSInE_l@njpq}Lqi9#-M?D#bo&FDKcobajbN(OH zFb>=YZ=cW}rcBrI6^;*s<3&BpbnTncxV_1@vdyZm7IYbaE?s&ZPyev7tnUwf9%a7V zNB`8%A9@B>Ipe~*P(&Z$9TP&gFKQ1m&WF=ut7~r`$DaFvP9OiudyU)0%zcfIpYP-Q z<+eIBD_Q3o!0?F!&%lON{I7DKIxxLeU#h;d2gQMjx%-!X$XCFGd_cSzbCLQ2rr%g_ zglrpXS_O{lr2i)7^XW0>{#{^8#>q3tyW+p>iFf##=D%BB^i#wdSkAh>9oV-ZzcL=h zk`n@2%Zar;9p{;_e{Y5VXH#DXuoFu+K7knPb;8znnBYuP3qC@RWxDwkQ{7Q7= z<~>kcoV)i$aPw{@G=%KOo+5B+`>-_<(<534dhEuC*PrLU3*TP4G4pX^x0ySt*R8XS zvTNZd0jvE@^;`7OCqWa*mxj1LP6utn+=!+SH?cwPD6sFeShL zEzjjU3wYb#)Sgwfsqv1$Z~jW&8LdYKf1g{wd==`O&h&BVE%{v#+IHZArUloz%9*p2 z6HU7Gqw|m4{H2K;pXfjy?Fn=8ttqEzCbksi!yM@KR*9apUN@&2*?5|iFSBkXdHs+N zufoPCd={V|zQ$PE%=Jw6Pr?5>z~ki1{qcM<5%{XxevrLc?(?;Z%U}+b4>FTfo^lf+ zjMG|T-|5F>^D=wfjdgnjo5<4W@j!c$C zENqiq^QyXMI0;6vhe&yJu zU-{t0rcY~6#3l&8-(auM<_mNl5HaA)*8t}wXdU$c?sTg`KwkEQr$_qpVg5@4GLK3}gi?LlIVwRa%M zxae2?t^*FWp9TCjG#X-VfX9q~fyR~VJVqY}9gl6uB=?c%qHg7kJVqzMFq8WL@aTK(2{F$9rlFSq*Wyps zTy@tv#coPIZjHr0sSM>;&x_}rUhtHsw&q@FT=U(Xt1f-Z$BQo0)>()4rju_Se*Fz> zi`suF{v2iwY2F><+MYCzB3E?Iqu0C4qq?)uyxnE~)SYGigrV!#|AzTv_rzKZ{+}StFs%E1^sFdvPY$mP5#kRX*}wxP8}e5c;&SQ);Xt(A?G}I(>%qqAPuB z+>*c7ce1xE&k5I*8IvbkJ;B%{)2IzCh&DrszU!Mh))~>M;B6+yf}NmKjge^8jkk7v z-S~^JF|n@1L@SPO%85JiKVi3{gKo$}MieeC1Q%|->^naL53Sym2H`+`hK|OoAL_66 zo|bCwX}8b!*PVBD@T4!BkE_lxA47?Lw%{{T+d*HUp* zPya>#*!Y~B5b=CXIzuu}|Jb40%9y|8BqZ;k)#l-Ucb|vWdTf2fM)YrZ8(H!z_@X5v z2EkJ;^FX;|$Qs=9XpM>$3PGK=ev@U)~ z9F3h=7gfK;OZ2MzXmwvQpP}gqp|i~ADBp=!)-ryzpIPWrq2gtOOb3RC0js*qbID&W z?E3AxX4bK0o~@0|jE!IoW6u#zPOq8BEy-)~nJ0O4VzU*71F!u4BzqOo!Hv7QS!Q_f>X%@|qhO zB>Sef&4yO(#JUkCc7~kB6UZS%98l*Nbj6ReHYUc$l}}38)3z+*%$4=FB9&M#3tz8kL@+Nzb_{GEBaC&Rgc?u z3wfi0zPtUaU>-xO)v}Y7@vA+K`hAt(y|eghR!lS{Lp0Ac@PFqXQO{RY5mlE`yyJm*mHfF){Ek_ zu5lKx6g?IsXqmBDsWGlg8XL)i%EMjJ10IkVXG3gZ2;QxGXnlOVu1?y%MD?w*Ns|&< zS<6_TwLa)Ml&H^!#?5ipUdjKE*u<9CZk)h=6hdF^N3J70wU=*uVMz?9GP z?%Ic~`U^M<&MA{+PW|Lz>lMjkl0`+Y?Ef9FxGt^PT6ZdWCRs8W_ss|C8+;^=t8?HU z9_ixBpwrfU6312Wlh5Pox2li%8XwDZ;A&{+ScX}r{KOsv!F>e3UDvVv8TGZC#{D_Q z@`6s>Yu&43ue5OOkJIJKP?m9QCErZoU?;!AkL+*BxTvMAD7eTM4Z{CjKI8CKrw-_Y zz0<r=%1;zp8Mn3ko3oZ{eU&^b8CGkqyRK{P)q1P-UF+|HXnftZ zux}-=>sr&j%y+Hpn)k$K#C^%kkI%2|aT}1k78BEuv=+zTX>GPy!{f3*{F%#tWM2`i zRxA2MNc+I-4)MvY;DP-C&i`|v>m2w{%2Jy>YQ$y3Z%w7$QI6bRK{?rLvwX-L=*`$_ zUajB?ag_zLs{@e>s!cGndJ8)E4}SjH>a)Mks?GwwN6z^^Q0?LUL+5<&C(m4{_&(kz z>m&H4kuj`XXn%ERFYtCVV@jbDY*odU9~(P#V$rXPhmk+%cf~T~8TuT0G_I>Kt{JYr z;;FRJQ{wsD&h<_30+S(JyWd!k+x4<(`@vThxw|~z2-~6)$5~Swhsi_xf@K*ncz|J9 z5)9E3vGJ-?_84d)_tf(EH=^Ut@%y(V{Y$!&r$IJ+d`7FI*zc7$CP?4k{}Zv#{0jEc zM1M@(8!GN;6S0hU=& zd{MX0R*Sa>!0(nJvN75nLE5@^FMOmM|I=QGv+07g70ht%>+$`ap%(&QW2qTA5!@Gx zo?QC;2L18#EZH|893IJ8-Dh;c!x32H(q_LB4AbIRQlPv`O><5XlYXENtD3d&Hj$cPUH|TKBIMY~48SUZT zGtPGQ?EZfp$LglfkK>;b@VMjn=sCtQw$@%R~;aanaXd_?C{;@52Qc*q<0 zv+ouAh-)qC^~^%z@~exMX0ItbdF!gOrOT$3n-Lexe%v?Aa}{x&#K>1J^$e>W-s}3< z3;w<_Z|G~y#Km~2=c4ZMGv8I`qr+mWojHu&DBEwc4#oQ7$d$n6=ufe-bKCz5ks%G& zm1&)ItF60+x%&4ee0rYTTRHn&^hw-0wi?AB;``;h*}&12=eWFAzbf4s1Jhw`9Nnm!8ZO&;1_?dq?|K19lx%(VHf@{tpz^- z&!xyy`Rw%%@qe;!r`&_xx{I$T8E*Ig)BHK%GiDEdN90*kT|*mAj3xAKO#f-|y5eh& zVck8M;3M*X$o5fyoY5D3y_}q}FY-TmIh{RX*YbZM_7SbK5nwDDU{7lKjGQ`#c;}Pe zgIDoP>xd(hCwxqW#74?zrFQhrh4pN|Uif*I@l;)i+?d_VjPJ(pKKR-9$Ya|Bcod6O zVp7^hp-afVwH$p`-#knFqskK3-cWvzwZI^El!Hg*_J7u-?kIy_mXm+A6uzeXu$;>_ zv&9(fV#u0&tNaSHv3UgJHo?iyNp8hvruO#H&bFkwl@|v)+GHixLOdee0beekJT=w}%+SPNq@3;NL7d<8JV z>t}8v_w8DK^O1Ss?K78?^UiCK18G<7<56Xqe+Kex}82jH0_Sd91d=j}gL+kVFvpV^uJ3r-{jxF~) zxyog8cX_Y)ZgXFsV}FR!epV>d^mB6eD^G2-C>>p%GmLrm2G1h+jGbqcYvRme@eu8W z@i0%oXNFs6OW!GH@bk6cmbRFTOyCC03 z^4LGz-EREwS@Jm%W1H!}^ALOJi3wDErO(N!7-=CU_*87dRCwwE^uWDdUvA4Tt9sTL z&d0D)iJR}~oWZg0Qcr!$2x3dd`RYFyO?)Zwaj|`7$rFsPeIk~(4_t_U8*&CCpY0${ z?%)rw&*^+G^p>jNyx=6+F8s{z7GxluDgMD|V)4ci8%ZoyfcffYo=4g#qx}cS9e!fm%_O9ZrGbm&bMq|{fDDd>RX5lnt+V`@i_XqJZ)@ScBm1&6^E}d8=HYy@g{9Q z_tgKM7@h;*TXy2T+2F!U?xRG0r;mX{F;1+X#apai)1u@L(K>nn+WHV%v;Qb(iCtQ# z7@FK>57?I#|F{BTY#WX4u` zY$u?%O-9ag)+ayhJ^_87D8&v>-|^KsxdHV)fV`?bq!xL@^ShhIfMqxOyOaBmc)n0~ zvm~~gi!qJl5no{$^j`KYLzjZVnC?4FIx&Ej{A!`%MYN$`_(gtaz;2Zg4 zY15(EWFD2pdgZQpiW5n$dlt4hi#9e;w`9_nz@Iz6Jn4#cwC4Yc`Qo`TP*ph6Ua|$e zd#>gGUIiVqu+B z;c{>N{jBHsd>5=mM%Ybmfz)xV(cs*quO7M&I9su;m2y^4YZ2!fZ)LtS@3p^1_I}P- zVQ(tuCNVa3DezZp`y0p&@Dx{yAKixiX`HXpn8p723ETaPwU3o^0FR>wF1ZPt!<-&w zNhvZ!od+3f#tp0oH`YTx@%`aQ$Zb|NGLf^!iimeRJ0{*(hvcoTM{pZPWSP zcfwn=o{ASrwhhS^&c1f*akE7F?svy=?gV@{zIMuv6PHJ|Zpx=BU!7=pAMt7}b(Gb(Hi*?ax&l<6iU}=ldDNUB%_$+VyQ3d+VFnkvrZYJB>BcmJ%*eCRB4`4oAY^oLKulX;&U%(}MmBvQT z)Nl1!w$K>oQYf#o&QP@zdj=(IIP*8vxwDJ;_7rDo^+z^&0GeCt8S0$H^&5C->92!}TZy+SOtqJ2e?fr0M(9H!zXA5x z7jnG@y@rq@XTYPM;!G0p?H0<&*Xz>ulu)SnMfNh7Q?ZHa*Wvz!BY77Z;m3M^tuc^;G$Fme!s2Tahu{)wSWTGd`g&t?&ABfYyfQI(3eztlXd}tZ_ZQ0jb zr5qr(^5>xwhUo{s1o_8zmZQtq$TW&KlI~Z*JM7g5<9d|xcMl*gHPn>G_vJmzL49jS z;`Inu6`uZ8@=Ju7^M2`(&<8Q3lf=*BcEWfJ0RFVYXa4+z@7a2_Y zcAC|#>O%J3rdammP1qrRgKm3H9O4J)+~>UAiN9bUP?P*de%euPGWXkZuQ{j~r*AUP zl)GtMIIC(1d;5>Y9%~Zcl)iYBnB(Kb@6+SmA^I-CAM-ML zi{_Yo;NODwn2Y>BJl6pKWa=z4rs_NFGrOGl)LO4u@Em6b*FitEo}q19ke5CHKU2|l z_Ct$?bLfq11nrz($)3ssX4sB=YjE4e{NK#`EbI|`saNOuw$Ww*IyHP!y7Yn|yi0pf zjn_23aXhv@id-Gea`MHuWueCesVmbAZhIG5A;SKd5dK%4i=cdtUgCII?;U+>o9Gl7 zzmT(L3duhZEXu0Vp4%WbuK9T#{q#SS)}M2QCY#wq#|D{ukq7O@GWdb`HuuvMSGyGWhB5vFxaRP`>^N=6 zn37-6r`MNwnOl@s+4(%XCV<_=+oSD8ayh$nl6+R&OOz@1c-yjNU!lF^w&?$0w=VfU zU0Ofeue&~L{)h%Ohun2KA!|FiHY8^+V*h-c=f~IT!>px?SWAh^%au=a5$ov#=&MJE zvBqa=o%f>unE~*n1-9&4&bpwS+VOQ^N{D&DzC1tY&e-fJABwzM;vLZTA~Zxh$QsC) zj=X`)!I~S(3d}d=n-6G=;xulm-(w%7?WxG&av>h|T zqr0cuqpY0GL$M>xFNAgi15Cf!i%U1QB1@DGC>W(RO78`**?&en*{aEejPaDd=VW9oMlT zop8Fi)$f$9t1}I1`7}Ww_ zjLQeU9;L_!;;#kRW2GaxJeKq0kP)(-ci16&>fKq}UJ)PD|3BM(6+cz_tlK8vga5B! zY2D+3Ez;m*$v;L(?^`Go_j`|~=<&)t#M|pq9ztP2yAN*VfUWoN_@Z!px$@uY` zGt2ig={?6R?fW?S3r>G?M)}J=S3dU(%Nu{Yi%;O$PW@Gs-+s>eLv@|-GhSZ~|C!~# z6#n$Ce;9ls{v9U3+@uG4o1~DPR4=9&bT?&-QzEc}|w^GQW6eR|~#D z>&O)ioSTrb7hlN%>{tKt$wRwRDbqVq=6Brtj$GOBA$EeGl{&4&^lmfkO~_P?2{zsl zAjo>yv+YjO`#!YVBnpa zP55^9q}J9{vj3^jq!4@P+p&du0w(iM1$~}=vn0(vM4p_PQ&MhUDV?s?+;}j=8634H zc(9H$En0_}4O4pAD{Irq-NU}J!gTY~!1#)q9CO{uFlU??{8LkgnDu4Y160Qz!D&+3 zHj{6xf_<#=^WvVwd){`Byl@P|)VAD)920{(E#l$^I~h$U&T zWaH(&oLG+!bvBIgP6HqPL^B1V4QK}Ym~!|*CsxBF4h^L=RPfH`zK$Gjf?sxgjkEmL z{sg_OC1>v_&UX_{h#q9inT2hsaD4fpf-TYOvHG_8jqgvU z;ORG-tv+oUy9#6dNp0EZne`V^*Bib4jzmkUut1ag1;J#-!k9tR>iuP9}04omvA7O)|neub4H0%_sPz>qfWcTLxZnvzpz)T zEybq=E>FvS=NIab^>F0~* zV{=T%c-^Q|G!$jN7ktI6-)5%2M%%?|lYORxM}Yf%pRp%#`@&tunBr$Xu~%LMoKugm zzl`^OVjf<~h|jlYE{e}L=x@A>hu}qVyx$2OGT%7!tyhEaUAe#ZO~sHNB5zH7G`4~H z+@$%OWfknYV$zMTsbAQzR|Hl}>4>C;*o%?>ek3*gHMgHrpw-ZO8nvNOmFb-e?>T9vFU#3dL%iF92z2004;9OPSP5O%Lc5%s1ntV6 z>uJ!wTz5W3@bd`wIlX?nDnQ@MF7Hk(7Wiiz<|M{I^yA{)!FBIAZ|lx?+v#H%dc_CR za3M7NNUx{gH1JYC_VXw)` zzt!Q#TLW9^uQ}KP?0$zwd!aYtIhg0dwRoH6Ml@3U<`02`d{sLt;43Tn-^KZb;9U5= z8#p}R1RP8gj#7*@eVKnt4Rtl#kp5)NBRTKX#K<)ppW`VF4^xNeW7-h2A;kRrC3D=c z)`;$zi#5&OzV!FHl@&QpzZtPCc{lw zVPHGp%+2m?HZas$-P`K?X67__?}&Y|_0KXtvP2`ykIkMHZ(ez1>#A8in|h>M!#18@ z#P{$n2S26n)j|ivx$vy}{@8{J_8u5nqG+9@z=}?fFw|gZPv9l;*1N{8#!Vp2_#*A7+P%4I43JoUd~>$dy z)a|Y{eq^g!=AR#)r*(ORWi|*e!i8|Mj+n}vTkbBqf*S4i<4Aj>N~%osXG(|KGvG z7|Kf~(EQYTpGKYTSpM=z%o)otF*X`Ui?!{*!Pt7?K6wo1@{Q;|0dq~o#yIU4@q96^M`>Gys949}&j^_pN^XPu{ z-!OkY4L8ykKm0Gw&wocg=%QVs`;=eUuTytZ%A^}#Pq7MK7e5@$_up$H7a)BRJT5&> z5IkPN+_SBKyq^wTfQQPIIDE?Su#fjIZHsNN=)=eKL2%;_<fPqB)sE9aePpo6a1^ zCiEEnOXeLae~>bcT<0+x9s{TN$_8)tu6W}~Xh&@ud_GTZJ(F$}JNMkN)o<<~SF-p} zg?Gi9s#o=iC;caN%=WB!Bee@nYR=t8?y%MqtKU%BWLSR3JGB>NomJbvBe#aioFeC^ zey7sr)J?JVvp4tmHaX<|>{i>puqlry2c6GFRF!>|I`T z919(1A9@etv#T5ThWm2kbL67k{D!dMZ1tH99@eOf(#(bz4E!YxeTV;%e-0Y_PLdgn zFMh_(I^NF#pRA)B()b3xv_2m`9|o8F9u!E^1pnXY3tW2=xK2awB>rkVV^UmVeDg!(?$BO{0P9*)?6H-S zPiCWk7Mcvsz6mw;V&M*S%Ioy*;g-*Yu{&;Q98-xy<=C=J6(SO#J}-T+H)6 z&xe{GarD$aA8dy2_PHw5w8`}OAdqcl_Ufnqdwo!lV`l!~WN_CVTz8Q-ajXe9DL(9l zhn`*YWPYftolp2y|Uc&YTL>;3zk*aj9GSV)w|1kRbBA#^K14l zeRa*g#cS7W29AB9qN?5A)V7O4kJgkfwO1k=RPCegfa(3xM|rjA=DxO;5w$e|<9(?` z_pDvPH2e5I-(xlFOfTmddBdqu-@ zFr&`k(rR|FrFEX6JkO!(K@Z6+drvoAyND zy_#EdwgF$ShBE(qHP@RyJKj6>*vc(dx8^G@ex3LdrV z!gLA$yYua3&Yu)ssN316DVb4awRUE+cCJM)%|;grvTpv0{IBkQzR1t**J{bp9>M3h z&S$?i$}+jmJckdey^E6Cdk33=+S`k~b|=r>`Ri|v%^V|Lp1Sg};T)i@R`B#lg4XV) zY%A-{rpw`ljKAXI^*(}pB$xx}wAhu31L(B7=yMBkKgz4_&@yYU=t2A_nI6K}enby8 z_J=*^b<%?2UbHV8MF@G_m>$&!yuES_GP}DLM3B8BJrg=>PwA}j*f5>L;llM-WVT}J z5PX77<0G6Z?nL#Mzia(;J~W)gGxlHYC~r4E9gR*K*8x4#9%algL677iGxQyVzB3c_T?Ty@jwiR(z^C8PobaDxoJU#b zkM$xWd6AJc*2?8E3%b10Sk;AZ6JPR)IoO-LxrIl}boHkc+4>gB+)A0>Qf3=vc2njf z&OF~nnOptaYD7mpC?j2@#6mB?)*^kx&)9r`T!#!^?9!Wo&Xz-OwsqT2WFH)5S-IW> z-AM;u`$_p5A#3`fxz^)DE3Ar|*`~54#Pb~EBOWL%_w8=dRd4%A89J}}qCTwTz3{yQ z89=yqasDjNBkW^#+wTFK+IK3sHoQBwp%OcbtMBGzW+95+UU1nRoUMIXRsr-J`7&5@vNj@E9)tv_vVktvGhxQ zRKNVRv6b?b$dYRB*9P64cMnpYykm`1*!=Uhdq7jenw{yz0qRxvzfx0zY+2 zW=&q3^}iFJNSvk(zxJdJ&nO8K0}g%drynID@^8`4%jAP>D(X;tq48wC&wiE##8kzf z|E%!z^B3dzZBjcWwzYuy%6XOMJ@bnq;!)#yG7K`bQ{SgkPk)z!?6`^YVfaCv&O=X+ z$GgpwEKR=HI_Avdyi;t8&3wvaf6r!~$&TmC2`!!tO+F9oyKS?e&{QxdJXP;_CQXEP zJ7%*_#p!pNSs)!CvlqYAOMk~Z-$QTcpv7s#Lv}mQUb)Tx%bI<}nP~pbjpgS_o-Q12 zs%`SJ`6})9)Ae1(eiZQJ)WLJ{4d?$U_QwBo|NoM@W7sW6A}6FB&zp$u>9nnO<)4Vx z725bK_x~JwvF6L!@OkRN^-c`0Vm}qjx0Z2;nzTHF9p}`8@4l%12OV0C|L@S^kRsxm zt@;RdFU9&Q{*bdk8(N5w^)p6JOg1sUik}@sKYowh+4*jx#(O}6$|%=nb0fq#?h04gTj*{u~?qq7zmse{mg`}-D?)l?IHF` zAAp7;Q@Ljz^f$v_5`8xlFRXsC_vgK*{&J=Eh7^Z9 z*w}d{88MK!ckqy=xvew2`!@&&5#9;+ih*@wuor-TcRZ4x&*gc5HUdYcer{~P$2-}X zE8rDQ8EB{L_-pMR$$Q{y(mTmr`actXP|6tVney$sbnnL48sN;&R{VOLuPDdBCho&t z#k&9Pf0psE{e}}v`2<-F@zNMSn+*Qezp|zGKX_>V-S1px-8TBOe}Cz_zh3sMhmL=1 z>L;)K{+WOM@pB8qZ=PCp(Zu{??~Y%$=iq|>7&FlK%j~T~{yC*4H6zsAHsZDWx-Y+Y z=&-xKU03|o_g^2?a%ts^o#rF|$)DbG`;NCJUvup@fA`*B8%`8mH|~)uIy~h!US9j+ z^k?lSSN`uwM}P6gp91Nz!%e^G)&Iki)l&w){lddvoBa>xEeclL@Sj<^D;~e{hpkKYZO^;&2Y~^fh6A^3`Nrgf{SJQXxn=n)|L~8)UwXPB^*^jzmS?>6$e5h15B0j~-hX&& z;b%YnOX!7e|GDPggKIW#p0}p;(W!6!b=&{m-1ptJy>jlp_`oF%mG@rrpY^YP=TDD! ze7AV|*Pp&~+n5)(kMO?w%kpn+I@~j|ZEE!kf0$nO>u$G=8a3dN9&e7A6d3lElXqTx zUH{&bX3SjFW6sZe-2UCK{Ax?ZBNs1Rxpx1iXQtgc^u>Fsrfqq4`W?SKZ~w}jIo*Er z{v#9m9J#0N_VhuUpF1z@j=T%5z5l8a8-DTX_}6<4pL|Wje_VgVuYY{Y;k&;w_v2+x z-g|f1y2S$)Z?8=A{PKz8vA0&V-Ere{$DZwY>(Jr_+y3kF{LD??I(*gI*}wSq*juLu zPvt%E&2PJiOZpAR|G#ox-@)0N?eCA^789>AXWT=_N%ubT7MywS=PRGCbYrFXjh7M8 zsC45>bUcVVmxeJ^+FQd+M>J+T1(+{kw z(+{kw(+{kE)0eXS_|4!Nz%`I-FxL>SY~E#Y1-SfNhPJx-;(hJQ|LT|UkjgXlTm2P0 zf@v-Ec@RPwc zkZTB+#!dJYPK2Kexkhk};u_6$3D>1uKA)fKGU`oFjHT*~<6ZR8kBj|)24{>J$|YJ5 zj`O$>Xv_qz@m%A$#&V6}x@^qYapNaU%$qd1ps;Al6<3bUySnJ^JMURIXVIb=bM7s= zd-j}#H!i&Uo;eHeoi}F@!$hLdwrsc*A}7 z&bW8soYFfJ^-jKe#+(KB&AIE|d8Kz;eb?z%)DbVA^h{q&y85QO=HGSq{ddJbH)F;W z-E>#k-S;dhopr|?o{haMslKGPz7SquD_u0_s?xh=-{As0W?a#AOA<|8edj%9%w<<6 z)n?2W)(7m=cg1(<)#?6^E8_d>=G>X+;TimbMevOK30>|l>T(YkI-`7Em;3Qu?(Hu3 z*_hY);U)bgTGVnF|dY9fNUthT7-Z?YwFTDq%a{nCbI(05g-<&Y6fHIG)?NHe zQa=&m@_Q6=hw&-*r3xck|yK{gd1e70wijgs+<)^U-(ZemMWj zxwBEb@b_}h75;QoFZ@uxn*YoBYGEe-*}{+J|DW79<-VHxZP7D@_544|ck+KQ`j^pX zqIYtCHs30IKL4kq-;rd@20G~(R%J1@_!|_UHD7+ z|2z7txkCOs3#ambJ^y`$w+p|N>lOab+-AO(|84nGg|9^aRdlxS4+^Vn%KfX{Hy8eR z^sV{7R(LA^kD~9+{dQ`3Gymbjx8=6-Kal%xq7UQ`bAKlPE4l9~d}D4WI-UQaXeamm z`9E3s;oNrasr+u?pGLow|DSVzC3-aXyYnB<|49C3{+FY_pIeObh3_l;wP=`oI(MNk zS@^l=C!!xMd{g0zxo^+?o!s~4el35ka9H@+{6h40{u^^&j{abwQFy%Yt@$(gzf}0a z=t}B2wH|8Vqkg&!*%=KlBmx#(v8f6M>&{9lYdTlkjf zW^SVJmBK&GznlB6{8V8rhtQb+ZH51~@Q3n$zwmqWpC{MP=Dtw)U!y;lTPmE+KbiZ1 z!oQAo!Iz%}H-04lZ}U$Vem(y$a(^KA$8x_f_oMlr%>Vhq59Gf)zmxxJ{`cg5J^#)0 z-goAIzA&GACEv*R^1qh*w!)vv{~ys$fL)!!e-~ZKe=+|H`5!2}5PdoN$^1{{zc2df z+~3Uomcma(zp*ft|8t~0o%>950~cUFo&Uc49|enE&wV!kyP{_FnS3*MnE!?7N5Hf* zx#irATqXLE+`r2Io7`OP7YhfB<_{LWBl@{$nGyem!Y}8Wg@2s;k=%?FR^B8OIFGiON8~Kmt&gY-Y|Ly!{ z^u@xT$e%C#-NOG;_?~>d@OultmjBf}n)bre`OkrS zcXPiL82@A8Hx>S7;ic%u@*gk!&ipSG{y^^SsGa-0JP&dg^FN>a^SLkOe?IzB?rCPg z?~a}{*A)pIQg25{%F1#eJTHs!IHlf z{lB?-Za24Cc(c&V{cz#i@?ZavZ-^dy^zlcY_~3^=@MPiZ@*mFOoRQJp}@WR?+eYaT|)b(KN_=^h*>#JdfagZLE z9^xM|T=0y@$>}`<@qW{1ID!YleZTcS8Ln7$7~;|P!+)3V^au6U(owbE6_@V%W#&ck zrL4QV@I{%ZnHLt8+sA{m{eEHX%A1Yu?C1hKx}IAdRtFRXUmp0s3?D-H3rYB9y;9q5 z58*zp_Bt&q9X{=B5`RBF#Tqh`@-tSbYuyN*B=sF(P(ng~KMCI~>LKRigYbQXpPn4! zkH6U!uIcvS=+Q5J@D-*WzZ*Pf@a6p4Mz>Xa5P!9Dw^bP(j_i1yUvu#z%^&51t?N=Q zc)hEkvnx}N3kGLw+(*Y3IcKHOYLNS+eAxAeBv9JG4v1{+hczyTSczws%`xjlFFn+pg{9NeCe;uAd9Upo0 zvB#hI(34;H;ol&#_Rv?7{{tT!0kC#)ZJ~FkJLp_kyK#5E-XBAUXgppbScV-x7C(k= zX*`TJ;1_nP8KH{W&!AVZ@>49bHYMf0XxSKL6T?n`mmL-GC$#76+Qqw>%1AD;ayILI z$uEt;>-FlOljtkP@+FlV3HNfkDmVijGI&s@jY_|N*y+_2K9YXCvlO4owddE~><=C= zKhLh+qy7|R*m4%ZiHwb@mIa>?TorQ17RAc7i z!hEx_*RI!A8#TwzICvLQeV)^5Sg^(+!>6P@lUd(HZ2TOdyx9e(*YR9OLN8{{tT7d7 zOQ*e;p<9`8=kwAdc<_iA@J#$8QDyA?2glyQ<43i5rt700WrycOPd)X~S6+F=c6kW+ zkZt-r^%(!2dhDY_68)hM@$ylE9(zn#^sJ0r+@2yxskmIA?oU0Xs1LpJQM+7e5e8;< z6&`);$=~=I3Ku3?`S0+Ju2d+7)`fTZ*?6phUGV%_b6Pj zepNKqNJT(h>zdJYTFpAu+vuy0?IC#-dg$~Xbw<-OYaRUY;mP!q+Q)2!rOHnekA-}{{ryPv_qhLq=%4zxk3_%9)4w6z$oycnEPm|kjM9#TkM7re zP|BPamqY0Q;etQ*qr2U$p&J1;n>&>%e#qQKe0H5IpAhf(VUT01el)@p3&JdNx$(y zJ;o*@@fp0pqoC%s)`g8hZ*097YmW$yFE7#hoHV~)V0X1Y>~=f70cx3gWiag3M>D9i zzJ}{LkoLX)eeVNOd{Yjjk7EA1e-p{mv&qxfM`tt7A!YWyFc!a>V`}~Xa(x6%EbLqK zc=VLS0Ys3eOgH-tXkum1^Nrl4r~N2*8T35G6JiKD+oz)6!u<)}wE$xoK}ZCv_@PJ| z=pCSl^{Nv4P23BQKKb~^cv%0~)6Z*$r(fO0^b0+JU&5Mf*oAoYa%FR~WMW+#5WAHAH&$9qDHae$%$te~S; z0X@uve$+*WQLinvYsco;cjkWO-Cue4XLJ8B_oF<0mc59NM+!OV->K+(N%My-$4T+o zuxB$+ej>O2PclKl%BUV{j_8V>{Q(rS>$HoocPVJM0fCy*Q5S z9-TRp%7H92=*b2>&wvVvxU%C99%(zeTj}+^^t=>3#|%&Z-H1MGZ&m)jZBL>APed*I zo2L7&vG+8_J^gs}Q_;_4(tI`gXPGyp`4`FiuR(df`tDcX{T}v5d(qG54&h_IWFbxa z`_*^<^PJ-Uu7A_>_b1OkYfpDA#aH>bhGN6NN0L+_q-uaEi4yMN1`20rFal0vDXB-K~m{o^AsihY=W z9f?2Kn;AWM{5O2!qaU08ji)v~tw64ur#~^h{;7>;vg5B^l-X3w5RKZX)HYsQC|yj* z)L42LPAT&fayx=gz#~4;6H${T6k8oXw5mH zT-7U8A&%Gk%);xPW=7N*k3YX)daH4Ad8{63%Y8jkJ?zD`(tfYezMav!V9&Nqp^f&Q zOD%odg6B9lDuexm+=_o%#T?FA#${M)>Um##x7jlb^TR>MG|?kQU+ULneX|Z^Jy;bh zYy!H8x_*Sm(;?K2rV9(4Z$O6cdjuvR%LhIZZH&GD&awAVeP%9-oy?vGws;cMQi%+& zu<;*37O(sLE=>aclz05DJOQTS-W|IXkKRQJmmnQ%FDsCX{VK;vB;4zW6cN~e>GQ_~ z3KQpq z@yy*}YosSqIWOBB#t;2cX8dGAX_vEet0D=yD}%?mF}-+B3^5BM)?5iWU|bij)G_93 zReHArQzuAppkB}G^a>ul$wMK13dQP|m49iN%kjc8;-bdV19)L!^c{U;X;1@FM#rqA z=o778qgX=ti!OWv;*-d_q=){Dq~GdT3f0#hMj{QqOE0A!WmHwBuFxNWrkK)?aP+cX z18d2$^dT1^2QWUgBijWo-HT@To0fpSOn7r;DBDi7MN+&O_ZHQqy&Vn1DGu`Uh+WDP}#TT1b`q(>X z@a&Q3x8rc@dg+D5`P$l6e`R5PdFl0on^POxQ>#;UU15A?m!0B)j;#%=BRH=DX3sC2 z7#|S*C!@-{m3RL_^o!hoNAzRSABg_Eg(!6VUl*IT@vw(^zxg%gdKk!+cfTciHM)!q zRJ!h+UlYooTU%`Ow8@KwiQWU=`DH9RcV?kH9#OFHE+i97%2PmfsNNNIoNoO^En%@J@UvS8?Qb37!Mn-ojJ4d z8Y!F{!I^^}pZ*WZ@Xf#9ZS-7u)5v+mPV?N{PNi06Rq8+u%H_TGuv|Slnwu+^tDSar z*y};}>SbbYm+QTr-x|Mfx?y`H<+YXND+@PombbRo=C7Fm5+?g!&y|6aA7 z3YTK9+1Q~LQ}z9Fw*yhNde&3JqNLuLFy+KDy47f-sEn($s9uYEb(+v;U#NLU`jBJs zm0g130mlvwVe8^bJ8ra*!Dw>a*^QrZwPD{vnYoVTKV)(23GV#?)Uq&-YI|p~V^+qk4&}IYIr?J0mFn{~I35~O~bmKm6Eqqwj4 zDy(VL!@_S>b{ca$Js;a^G^ZD?caU8xY%s@_-rlEp|ALQDF-pQj=*qS(MmnWl>T^&4 zHATGJ>BZ1_HbXhN0Y>yJqy@EX*5l}4paU5`5pZ7L+Dcz|2Suss&^Ec_@x;Uz8|@;u z$hwa;FJw;M)k`68Bwafb8b$PK@qi7VVWkPO4BNNkK_@2tNnr$2uY*djQE3lAK$M;B zkxazDT{)=43r#FUTrqpYO0O1Mo{{!$t&~8>2HiAlyGFYHQutx7(uG$Xk5chbuZ|&} zCT^^r@AN9YJF&)Pzfr5zLDgoX;!C#V5!`RPF9hzi<7%_h4t=6_-!D!%J5->Ov6l(& zu>%`jo^%gY&lfe0-23N;?#={=^)&SHO@9X)Ex~T3De_4DhPt_aV^^{>?9sLqe`8+` z>#mYTgZ+l_hVOuHEgHVp?Cdaf3A%K8{rGnM4g|uDqmQr2WCCm&kB;7HR$4o?N_1MC z$o0;!EzBW1lZmkSmg_Ya8%4KvdyRU#_EvaTGG$2K{d@@TbUL^IoF3vrg9gSfhIdWH zYA~RS4?0XEzj;9t*COtjK`QqvyP=@C-Es8D7vv}%=g@37hfe}{^0k%qEAuO54?ddo1HjElNz8-N86C4V#$ik5(Xx6v z*KbtIj`VndJC)HOOP5J;EZ?Q-ex>((Y~gw=(=m-f--0ju@T71T8ejJHYXI1vD9gb0 zp$`Ns5O%rxrhp7i=R~+Z^!>n10U6xJ*t;+M{Qw@#!aCvCk*8mB?Wy<`yHfG*e*N+2 z6G2G!+ANeVmi&kAsoE)54(a$1QMv2Gt05k`UG|F{K~;Z_?|Byu{d5o7^VC&)-QA~s zy=phz`z`S6Ey9?ZcGc=l7MELZ#qKq#`SNOan0+8uC^4Koa@bA&hR^TmkAo9OQ$9h> zPr%xNPv<9$%GOn1clXbRcVGX{y5J}ne7xl2cUN4*?xw%{a(8>~{jyKDXH*RO<;%bI zmWyxtak+HKCG@wb>Dv+L@FzbRTZX2GbLo=L@8S8^sI~0Ei@w~Juivxb-NUuTl%y?R z-(WaOkjo!O zeLv3z9*>N%CEdXHx4p+IPqu8aD5StdJ7Ti^yv?;$1bSsO+y{ho7RevM*K-JueL%TPh6H*hek`KTVPTb&#;_~ko&bim{p44o=(zG2v74t%QVEs6A zfd_vn{OoM{UcTx&(5*MF50!jt9}a9ds2|0Gb_YR10;wbhvAR1FDpjYI^@WZ3t?SWaPdv&Qphq5ilygxZA$)$Vv|PS< zcIsSl=EVzCbz{A>e3S6~eYark?>hpvzwZdh{=Q>C_xDvBrBiC|etq-FBcD9-$s?aU z^2sAl79*D%{Pp>po=!DApWAfBL`|R2liTRu|9iHIxVp_Da@I>x+dcF>nTe`Ll`@@x ziebtqK{Gz}QoOdk;>leku^<~%Hjz^4ciVz%)1%&NSDGG-0=Z7;_hk_-62j_9_%0(o z%96$n%T&D8>BOx{`;H_$l(+3cUsz-#HwTh3j452h$vHo)H5O!TM?2a20FI$1_N>u~ z_wA&-q1yA+stH5P!oRmvH34y$GOg?hIizw}ZJnuYMz&RBSPdEI(IaLT)^ zg4PSrI}B(uV55#1EykB$ie-<}_M-;gwwtuCUgr2vZ)dn0bs?Bo_?4^t9n_O*MbGvJ z9fYqM+-gMxXUmdlmf`uJy*C$cEib+_(X2fmZ!KJT>CDkY^T^ui`eZKlaATFDT-Dpm zI&ft?J`<(xh2L_s!osP}Zm(V!?T;_TrcMd@hjswUK~}yK#v7&Q=OGe%Vph%H0;4Em zVD;>F4(q+n?rc2S1qTGW+?t7>i)T&?n${1>yc}0*Z@YI|X@acUQQ|Hd=J~cJn?hcyMAyrFjfGOV`?$y<*rNyQB#g*6M zd-vkW+jzrJ*v8iNO+w47+gnRF%S)S^>zfPLmlj?tuWiInKOLW(Y;$R0d$Y9s`Vy(! zz&iLP@(TK?US|glBK2|)Rw5IINgO{NU$vsvZ>%kCmKT?o7Ef#3{6JZ~iWxh%`|{fLrOo9nH9*}E@Z>@HHrKCgm&mcc zxr*efjA^|dly9SSeSULkv3zBDE6I|Mr>iImhMLorGBsS+Sv&5ZZr4;D_l>vQS-eRx zqU^GI_z7aA35XfRqe1VE7E(|t3~YJ4V4uVnJC@%mcZG@L*}U3U)4wWKZ_w=;!ZP2= z-5#`_E-ua({v<&V-`&P;r;NhZr2$Wq$V(xza%7R7?!Z_dG|+C2104-N3G!%)`+(21 zix!mO)|lB=7S2JW5rG$^2i;s$A62CbEI0b)7LICI51^M;)7G#}004-V_P5b?3tzVb zAB*=6_@s74<4{K}T4f!vfa2xK0E)tS8&5&{&{#w1ORnJfppG)T(|+lbGoQRHigjtf zUg^f|-bW@p) zz6O^D9dUM==P!LyN~}-9qjD0#muqC->P4}a`gWw)Cw5y7q<=t~R^@0cUWIdAs+h{# zgR(k|B^*tGfksE{$paJhhdb)#{#c^_gyqq1D#3=-wd4JCUpB4qFzxEvSe5@%^z>*4 ze(ha6?NHjax!URVhFx)q8uj>~KNV}jCVNC_yc0f)O)PUr-GqBlhdn4ho|HV%S8<+- zz(2w_X&5FdohEYM1S!Emj7qbr{J?klX=<}s;b1ygtfRnyxFiJFQCpw&;*(?&O*HUc9ln^g0XWc4;YTzv!&>H1tLM zLGpeno|N=^I-Z=3pN&_Sk*nS$TM~afJ##S`rf70s`j{#|` zv%l91W2jw^Nf`RmUe9r2JiB}kj6WrW4ILctE6{!9bx=^K4Knt?5}z=|zvFn%)*<{j zb(Bp{14b%icQy$Z#(sELW9;|N>~*qoK;J$ssm6`3@a(j$yT?W*q&Y1y$d9!0+Qe%} zfQUme2J_h13IMW`#`1(wv=%;~P%v#ASK2!aXM%IW{~hG6+Y&S2rI_yMDQxKcG&5SS zWRdz2GOmH7F=vkBp-qrki3O88z1zY+m_M`$<@uH6Yis4z`O<3%@&V5kAPQl0L-XAc z9w<&icmn#=Ynk+6#NA*AsuJ)bg&WdkFe8&VP1kBrg8@eyQM@T_@p3{KtpjbX4&XEO z2*BQNy)$Ka90-t9h;?JRRe@zQ8b@zdZx=7`-1ap{>uqxMd-WT$fhM`Bt6@9={_bH7 z*zX&zuTh$Bh-$+rJqIQ*#@3WLXeX@nY&q%74DEM0+{&=S)<#C$h>oGMwMn>mET+9P z(w_U4!>24yu8*d@z^lrq4s^h~r3vnaCd-O~?X#|xlP($bm4r`ch7a;!UOA1_vi=Nvr_xFbbd||fL{D;HW$_e~~M21t{_pELAePaVT ztTWUf4-Nm}upodQUpIriG#(!CKZFm0cGB>ppG<3UY_5-8%|rUMcijXDWFSp%(=Xxw zJn>id6JWcvD&YkD3r(;Jw0iPrdVjg_6dclfcdZh6OiMYz2h1^-UKL1qCu_D_58)J_ zi^c6NT;E=Mt$clPvs`*JMndj-iYe~NYM;q3#!G{MfO!o;N=1kD~$^J*G(D=UbTP96THrmzZR4f_iw1W)uX|J&qnJ`M@aFh4|*~(4SjZq8Z z3boDayk#`jsX};BO4QVB>jA|hwwL|jsw;f@B!u?4rAColmOc~0u)~d(rLW{q9eX+_ zxPW)a`)(~&zCHF0v++R}AL^CWD=58qUrZdV4SKh74M3}MXQ!S-m07dZG*?ofH%jO> zY_^>@T)&%yPvO3^G#d#Aeru=SlRCa%ZkiGV1?T`eD z%Q9^UI#CHJ?E4+Mi88v6H9ftxNyy6bmc~{WJx^)W$*Z77niN`;Y0yZ#IF@t)402gb z5_GwoK(=xw_J-pY0xlgD1)ZT^sE%kNuH*9X;U@U4Q+8CgAT zZ!H*vYpbp)TW>BdZxS-mpPmp$g*BJO5AJH`#y5_T!QDQG8D%5s#$;-0+R}A9{l-xb z6G1PiV|iPauE|i5`8)EChMhf*YO5PhK7AM4wz4d-`nMX`0~RkgYDaH95B3i3D)iO> z@p{lgqer*Al}ZGPw>>4~$u2s1s|0_Na43N`ky82dHk)bn=5!D4cCQE-ayh6!q@u=7 z*7UI5IMU0(G$O^&9>J7d*Y;~Go9mOcrgWp^2j{A{9h>5r&i95>w0yVwcc)}qItC}< zU60kbz1QmuyXZ4rjUS?%pklU4s=_MpSfDQvyJ0BYvDk=yP~(wVX)Kj?y~Td2x~A-7 z3H0WD+9$bRKT<&Tu;x+7?I+P`OH8C|OQJl?eN{%As78pii1}XCc)IMN{0Em4q}~)? z!?=Z*uqk11L!hQ+(_hVDrnO+bmRD4)-zV0)79>9 zy3?iy$#cJW=25?4mK_d`m}SGO|;Q!6R@**~T!<$isSmAqCzno)I$DA$_WDPfNR zSbfE73*K4nQ>^04nFg1K=B>l+Evkpc#6e9%9UV$B8FcS{GX9gf>SyUf*B}`eY zIQUI(>|mw}X5WqJc?Vv$W(|WMt(O~E2Kf1vv}gXxa@d4HRPt+X&9i~IEo{OnVdIJ=o5X;+|nCQ)sj(xqH8Gy4)utsIN*Wiw(+uUB0RnzpQ z{-RavI^%0nj;6~%LA$^K60Mt%`t0Z#_{b=nZL+hpH6iJK0{N06D~Y6YpK<9!In54I zn49?Zezn)Y1E|dyA8#@Q^d`;_MUO-y(<{{5o86M_jzb3VZPL;fMcuH@(F|kWa!AJx zdW3v#Ny$V6qm4drKbhKtP7l#0nmiu&C zjYm`Y0yQfQ%eBuS!Ug?6hUiaw10&(A7RjO)r9(YK`L@9vdeiSkN{$^%C;Sq9Vo_^0 z4WLhv+V3feUl!cF9cfg67Y^NswC{5S1QMa@o9Um=ZIFHaj*VYts0`!@@O&OBWBPbk zGF)uUgss)`G8Po`r6m{c$wzz(6QH*~6USy3XgeLIXlJ$P+I+WjAljVxq%45ctA5u5 z`d{PO~LuOGe`ig;iyL{Qnm9OVpi(BQ5m95RCmA8tQ zYvo~ks#cbT3o9=%1kX!9;nL}CX>IFRLajrX!A~wVcW+E?)|(ZtD$C^Ce3*PapYMqq z^P6kSYuD!D?bLBnjC=Au4hc1f^~wHeolnD}>PW4po>y1qua(L(-n27J{jiQguu_sB z4rovw8Ok{Jy<>J%XP#{y#W80CoLQB7wN=$e-#vOlD-4sjcPEphyI<>#yds8i@-3ct zcIZAY20y=3o$7a{F1YTGdd15<*+wx5snzCId46$GQDHxIYR6eR8~)hNj>~B8m*Q(H z5|Y~Oeo#z4F^*8}dL7OI#Zt_AW>{wUbS&LCrk{{UC5y)bGk0{Lie)e0ts~5=TVWkM zl)uh&&7RZa!peN92^R!mR4fY9GzPh@l9c_)J zu2bh-_Y1?)O$uhRF7JHXSi2BsJRNbeAEZ0m-HS)bf1Py!lP*4z1)_^ z=f1W?$3jN7x2^wE0LglE()#Lb31MeYrWiXu2dB8cxn-zEJqFE;oG7&-8VuNbc8eid z7-PLP1yNP6xXD6tdKPOuBa&g>fCS=m6p5zF(Q0Q_@?0!~u1vhyVjJJh0A#|AAY=B8 zRV2jFbra(|%6DCL_U2l1#N|QtQPvKt)lm4Gv(_ zkEFpiJ4&ApT9UT*q;#?e4V|5}09AO+?LpZpWYi2|IOzk+gX+#p6E)9Gvo6$*yoBZ& z9M*NmS7@!00LAL0Q-wIa3k$pBN8CKr9A$cjF*88IyHGzH74O z=9G1^d^Bs2+tmJSG+q3;iT*Q0a)b-6Tpj(8`D_g$_tdlfXN^8Gt6vvOnkt?EM892x8_{yP4)5Zu`KPe>OmquS62HDY>nMtNoYV9Z#J_k@1J8_; z?CxWiVH#)&nHyVWroL)iE<0scnd8A&inF9SeRvY?Lpr-Pq`8yQJDQPAkGXlRqfINW zmBq5AtnP@eoAaA*l()?@D3g%%q%d=A# zrp~^o3~p^w`z*VoJ7{7(8H%K9oX*%E+q--Sxa5&-dX#HBVZI=xvcS;GFd3Zb5Y4nx zEa*kGJ;K&nvktq-0My||*ax%FR3Dzwb$9rMAV#9kV zU%ZA`8u%Luf>*;<=Tj z>A(+-r8l29f@}HEmZdorI#S-&)fYy~8|=P7KK^EMjnr?J^weiI6F*x1?AZCJq?*x7 zm43C+$YgZY$+p*0_~2-JZFymRap}tPH3!4W#qNzgppiz6mG4u8aorj64MSr9;Vt~p z6-iHU6Q|?pFyZtHV`wZ&cAR`}o$O8oW%Tq7q0DsuwoU+djJc%J8113&N5JT%IH$I_ zaG@bj5TnpOP8p4FXlAz)BkEgrE<<2N0^_MAUdU~7jG?)$j~sho^HJ75q22w4&f05E zJWH{daB3I*96DiD!VV!N?SMI#1^uMK6mZ)wgs;449BwE&iMA3?Bd4-ecD#n8$ubpd zaN4AY>7};EwJ~xzKB+{>ztm4N>}j9!7HoBSZHc2Oi*7IWE$c#;u82%3xF-YBEGeiw#Yu!vK-MT&v0aW^O+wW(&gpnyJVLq`oFqT4lIV=xTG7Nx-Ywv<6 zqrg)*mPjN}MUYM>b4d2I&*tDKnT9)wA&km3KHgW*40e1%)k|^Nc4*IUPuqh)r_Bpn zZ)})uVY>82NlsA&&y6%-5#Z}~AWnLZ9ae!8_a9-IQ7p0jf)OA-Vf0-Q#0AH|d#;=D z5r7a69B1xQUf_gUX$clj#vOEQI7S@Bk2`)`RE2o8O>mag?cqd^+oJ;en}lOM zGcD!^pJ^rpTJZ^dXTqQo{6>n4#-7j}I*+ALvJGqJqhX1yBPk%Hxyyiyt|a@{navzv zQBNoZ{q8FoFmp4wlnU;$8thnMBzl0{tj!T$jcDOS;{f&%)l=wY=s32tIQ#4~s2#cmrkTQBtqU3R4>6?WWhblniH;kp376F>?r7VoXC6hvya#6$bjbYAlGsi07H z)H}f!^nzLtIHMvLBgweixY?$(HO_lAvGyr7qcDnDSA%;uO*@kkWZ#j1i*P3C=9|q+ zoQHW2!O1u@k-?gMHKc#1wIeMDQ^mT#ngm`1e-3IZC;GJwuU#V7;$>~bn%Wx#l&Pvm z-~~7a%-CW!a(3z%E4sc4y}Q~46HPkPZAMhQgXtwn#7`j5ZrX;2a z>Y-#0Sn*3fJx*Cn$3eN3HcN@(bdBd1W0MV+a#!z0mpTw z+Gh&nF?5Nxu&V4q7`hrq@~^vrLC;LienLc&b$!)Yh6FPTCYB5<+Vuh36`D{R7#O0u zxBfK;m~x&exXwu<))zvH)b7#7O&iENb*5Ke3IIz#U*l!Ht6b*0FvHtjT^vy-(%90M#k&9N^e|PLVG`mP88f3m(Mv85)_)$MqPkX^aypi(!veDEUAMl$6%2_4J&05^jCncp2(1Oht z8;2EvfE&qrP^jF851=IDez44d5=cAa2e#x9SOhyet_hu!hXJs<1QA7UV0H4oOEh4Z zmGmuK^Qn)5$+ozXLCGw)(W-ZDGfJH^^u}%A+BQ0~G!AeR{Io$$Y4}KZqH<2^X>gP! z6D716tIF3^Ugan6U2h!(c0|pdb(+a@F%3NG$9bRJ3yWzhD@CDmvG+T4Z)nw zK!IG$Pq&e2gbX1DwY|(+U99S(1?=Y1+VV|vKgyu=ItPWbHu%cy+GhNz z_@=Rpp1v+muV<+O^PEp6#GfB>ItUt8HCqp>3grN!F~odPnvdv)*<<*ilCPi zUz}PZFwpM|rtnY3jI5RP;J=K$QDZS{kmW6S@Q%RY1cKGk(kL&9E z*8Iwx;f~wEc3qFn1=J29{{;K+Mi7~t$+tv4Pm08R!~Dj$TM7Ds){Y$xO8bfPL>A)v z>9A_Yc{1@nWK2HaH4_01o+d|nW*z0v1;`F(w}vJSzlLqLWMYu8a(LQ>+3t_@b@D$=eY$swmy-j2Qd+r!WzdP^_T|DdJ-3cI<2Uq*N*2`N`E527o5Vu|C#rFs3@OGLuqv-o#n`cj|_2ja3p=W8#Je4 z&ZgH;k_5O)cN{%N+Nqk9(OU_CJ6)OT%PGR5%`YACH*em=VoUaQZmvGtZj9_t`orcMMPRm3#Xc<= zQI#sWN4A}+8GdT*NQx<^Gc|Pe2q|@R|8^NSNN@?J-lJc#kjzw1Hbhd7TRTQ#`>d; zk+8!R>{U45>i;=P?TsN~>>yrS+KjJkUpsY5zKR|FH|TRQ#FsBM@SO)EAu2-k!}78oNvdNxac1^OJx2VbruMrML(xD91;lyt2Oj+V%!U>|5JZeQoK+QfW&>U)-G2g1BmEV@dyf zaelnRP?fzM zb8^mxDIrcBrLIg$%>vf}g#A z%eR&xdLqrI=_cZD(_6Trfe<|{nZnu4UCBhXh6Hn#dB+W_?oO}d4A719;5Z1fd#`SO z+%kku@J48#1Y1|fOiidZnkV-G6ITys*UEn0>+@Sn)GJsx*}gUlT?6+}HdjQ~QnO9b zS@^UNB5QJ4=466gH?!Uuq-gv={_|r8^rvwqwZ6F&J@4-mm5H^9U4AuwJN)|b#C|*h z&h&U$C{EmsC$@O1PB{MetS|4pzhCrsGs@!`yRYeG&PC3gyKsIsZp^&Uj;}5*EXWw$ zQik|fw-#omOAE8J(>Jcop25J!?}LgKLjms^Q6|U2bSHkT&VE>_(c;)qz5RS#x;>bR zU+thR<*@8Ldf@nl=i|lKiZd5x&d0BHcIxf@%0WAJA7mKG3p}i^YOmC84~KhGHR+X~ zJ9j=lJ2N|5yl~ze+}iuu+4JF@`dYnbyq)dLi!WX*&de5rzwn&JM$u<)-FZV6MxVXa zUen!q%?AIv6N8C8el31*t%p|#hpq2;zW1zeQnY-H`*A?G;cD)nVLy}fo8U>B zktjEFA%6Kj1EwC2=k61E`hoDW7=XTyBkL?}#S=j-uUR*%p!b(62;v+X>T5M}rw+_+ z@Fe_7s&NUVp~)Haz?M@m@7P}ce2S@0=TN1TKUFnble?#4N*`-#;nFay= zscawWgO~FJP+Bc?Ey!GwMv`F=A0wSZHU|d`j~w#=yqn$t$t$#Qhrp3imKffPmu2F_ z*$51KsZ1s$Y~GR|zu ztr&4J?ljEGH)V)xoc4FXAq(D>DYx|B)@;|varnC^T=s=Qf_J=~mR0>Xp3fpkzc%mq zzU8&6ye0MA^=c#Gsi8D--DG!4h=0r73_ptTtobCcN#&!Rmx42X!mwFOucEKi_Z<2< zqoUc1zRTd6*0DP^Xmx?gUk?<&SeIhQft^&QMbEf-d&YoIB_si4?WS*+;i;Q+8OgDrQv5)14K2|FC5kaGsZdl;9z)F%FhV=k)3VSCE ziQByTT&Mkvw)#IuvS*%mTWhJVMj1!)c_GuCsJ+7xPb5t%e1hFQeAaoJD=8gD@|lev zUtnuse2A1NZPjkaX|=R}7WN2Kp6NNY@#%G(>;tT?yk5S(&IcZFN9QKhID9+rD7r{r z^KB&__A0#{ZJB*8=$$;A)$Nt7WtHRKH<#8o7ne4ZcTTI9w>Fno%Wey!6wr65*TtM);DEY|Hyn*LqB8>}n_8SOU{tOC9eL@We^udQV=3WD()6(zer1 z@wwgC=a*NawRPN^Y_Bbb_^r)1=C6g#76RQWg&erPw#5-JiOzPdE^V5TaL_nob|RIo zs9ax%D`$N=A3mW#X^%`TJkc))O_DOX(^jrQ+PUBm7;Xc!hNKF4r{#4iO`9AvdcGW; zzm#v1Fo8~a3zD*Y9PXoS@adjVDvhXWYU)a-I4sMK(bTptO=xq@pWEeQ#T7H!n|!v} zHBT>!&`z4>nl;_hs|5Nf_;3^!#%|HZ_m$+(C`KLmx zEX`zaQ`Q$+57bs95-Wcko}Y5w{|r9WFr&%tm(`lqjY)lumFxR4lq_G<*^xS?H)8{m zPb--#AhR~2UeXImO_iQ92g@-6V=B8(*49qN@dg|`OgrDdkdcP8MrLuhV|%{wrTxL6 z+n<}BhR535yPWOuQk2#+O;d&%PWihX{>IM%`abj8lsK4d;nppzU(U`~ z=I^#)?hc?&CVVya5+cvGvnY$2j9G?H-%_1Ih5!#Xoz1&68|zJ6Yoc*?AR~meD-~#l;SFCx(6lnZC6w45^E9!yQ?Bp2l$q2U^$GSJvk}rF)QmSXrAj zX)#fs4C!a312F#w-C61Bb5r(tM)v1VTn(*ah5nc~m^HpS}c$0O0>ib!ki z^?N7UShIBRbDrq4yHm;-N;P>%UK^yq$Z#f@%DO`l2mfg3v_iK3Ark-`YPzqv^}3z14)v(%JhMN6@5&IM z7gx=wq4}G67hD07_Y-zRcSE}DdGWBf!q~GF_qzOfGKVux#(+&beKNnkId?P3+Qx=^ z)i#8LL;;}IdQB9qQ3+-N*qsIti!%}VFxdCJ}u zl?jj!Y#)h3^A7ddg;1i^*KuK}MNcg1VUvb*ejX$nOHO}}2fXs4xm<$9vTJ-Eh5YFr zlk*z}A7Xxme749#!6Z9k-30#TQfEV!@>ci?6L`8Kc3Rnbd;Hw`3N`x5Z(0UIhk`$r9!A3 zs@0dV6f3zsBa|D!2jpY%IiAl+wK|`7VtGNac08kEDDgh|vf7;r7NPMPmy3Kv*wu7*Hs!qEgzP-1u@dZJN67?&nQ`M1*0*Fm zbit$RD28&dvu5lp`SCvfjywzRFoPGfzc4KdBa--If0~&7E!Yj?qU=jKS~K24pO)lF z)mQR=>fvUTNYpX&{a2e?$Zy`3-QZ@-#U{+(%lk@8%E)}#j{=6ah*Kp;qFM_*|Hr3P(7^Pj9}=(@GdS}O|f_FGV4$e0q)(* zos`|Zdjr`d{T^IB*5_$_^6Jv}x8Ia8qH?!>NpYDp73yFwsvAJ7jbhO(MUYJ4IV%IYgtyX8J;*!J-Jo-wP##3>w!kC_UPLO^b8FzDv=q(Fq3lsT)ZW2>G5ES8o z_iLEo2Lf|8N@$bCLno+UNhFJ98-7_`8%iAaK&W<%uWMeRek|N8l+Et0jVo~EOs5vJ z?!X^=PrYLYW%|zvy!wa#jdoE~WUA8ZIw1uRjqZh!_p$l!%^&gOHKl)m z8Ux}(k40EoSMo>>2j|QT?j!W+Ib77Zy$KC*FeHx6k&wvw@)#Z*BIkGKw{;Pb)R4j}D21i3>P1)m|L1 zb%J9lnffVhxwaSC{h(^=4QvITkf1hozk~qnSeTXqKB$V8=y()-g$=6JQy6h9Ni?QK zP8m;YMd0gX;;-$n_sN6;!*PE1&;LMX^gZ&wCsX>+E2bx0hKeUq!7<^fq|x9A(zBCvr+#*UCxRSGU&|WReeD z^LuhT; zZC;&USR(zJ_fNpC4JR6OA}l>i=h{V~%X~Y=(jGrzy{vp#M#W1ysLF8>PRDv{L9Nlr zF`CKE`BnAg!p8RL=VP3<^X!*xYl&r#KNWrJ%P)Vc0(7Sr?Ahv}liMa2F>i4=FRJdNNelG@MuYg`rK!&jOmEW>AYrPR%qXu=tqZm50c=< zp@zL8Rz;y63eUV+Iv^!s-a?;kRVG+_z(;AEYgj2ji)?3ey2lFa-)?m8^KN;H=8Tqp z%)oK!-M4i{!hKo+F8T3czF50AmrDz>TwLqeh(!yV3+9oTyT$W~_u2CZU>EPYj=1C0 zxyWl)O}@|J?|z;P{5;y&ST)VF#$PEe-kmoNg?VgSWpjQa8h+4uz5cF2-mkie*BZF{ z9cR`T-SP8r!G8z+&De-N%tFELTjF53*&NrX%3mA9%czo$o!9&X6)F!{`9{bNnoY*_S zrFyT`1iS>O-MB&<*q24nswp)|bq2IgVX{Fui*x5jpVP;z3hf>(tnIP|K)PBZhBn33 zE+4zcEkk^;TZ<3&`3?ECs(jM=fO`$+FPvCyAAtJ2;>7ZxMcw$*)}KCq1}#>cCckRj zsNtuiN|pMRct76nc4_ZXT&0k1T;-|7x9{RTmcBz7R4~MEqY#d(NN0Q=g*`PmrTAcn zL%K9qj#%i|T62#9-;H-KUX1zs;u(JDDS*L^`=4*p#&%;DHJJ3Hyxb=9ArM*P-i{Bd zcI!9UmWE&955o(>Q*FhQD!ZALq~Z zwm&$eA`DW$OCteW-D|{e6VSKl%HeI~zZ&@~gz_K#cNaFD-&y@Rr$6WQM+J2G--a>z@Vxh{afW^B-RJL!EU_Y z?8ZlQ=E2TU3_aqw0-e*`VV2PU zeE9|88Bu%38RX*vfwt~Y&OUev+$s@Wzg$31+gi?|)zX^{k;HEqR@#GMi}lc!N%R@c zBAa5BETUMudosv*6SofB!fs-Hv$(RpcC9FHc(V1@wT)NQa4%XK3ua%WT?}ywd6SQ1 zEWoX~Sm-1gkY>F!gsaoxsK@~F6}>8a^gGJ{C$V1EIZX60^G{{DI$=jR80 z?<{V48GkfDgfGbEg(qf*@fnCu>^{PAEg0Jnt*Y?ylI+XUMw9RCY!RL;`ADS6B33WY z;muqajn_7#GdPnz&=j~Mc~<45jOEcGGr|yM=%QW=y*qsE$GytW1DKiy6&G;pL)^-n zSMAd+i-~`vk?KZ{>*2dXUQn3x{LB(|h=51l8TG3)-*gmYOu|Y;-(|`FN-hfI1}NeeRQA9vQ3jyX8e8D-_QE{xo8=O zyi9uP#ms^}_`8jf;BUOW3QVF=PZ7H<28=%?8PQW#ODGy^t%=v z^$s1P;4aPr_^6uQ-(23X`(2~f$kSt(SM0g`#oifv0ACY7>iV=V-jclKnv(=fUS@|R zP;^ z;9x`#+gtV;ZQC2~NA7(X?w(ICxoH;N@O=3^9j?#8yVS^WOh2oZDLEOhxWj89+Wf_2;RX3B?Jt*9 zu0QC@qY$rp$P=QkMhK?-bi77$#>E{Zga^cngcSQ;K~Q2OVET0K53U1C501&u3H{ri zazH_hWCu#}0JufCppkOrD~ge}I(I+cJGyiCFcKbl#KOB2Y$zV05=9f8%*R6D+L}8J zFYsr}%OYepyx51&_B}Ql-rWryHWuCr;jI5=_`n{6WGFeAHDjkR-t(C-WClyjhDhR| zkM>B(W*^$ZZ5OA>obtzJkb2rrrf#LtQ<>R%jE`Ht-834fva{)m{r26l`XYkEDB`$5 zu7R{Zt2~EtG9e0G%zTUwvFR0e$$a##=BT4rY4Mdsvh}fyg)s!-HSL``IJCcrwB+mQ zkDsS_r{;~8rg;|Tzq2?Bnf=&h=AB=m#5~YAJ0hiLBuB8qM~RVImwl z0)bEiiBCqy5DdP?c)!v1Qw#QQ@VOhD!{OyGxIc0H>$<*E39p+V17j{6bq1Gk>-tQ_ zD1o%DO6PI9I&+dz&WU@R+(&0-20eaN$4+aoYy9P*cS%|E^jqOG7gB$u?tMq3)vn@x z-Dd8N*Hi3xjM;I^Y^1rb3*4~#8NWpBm^FvO#l17nJR?x)JAW7bfg`B439EQ1;kf+r z9-S$=cg=BKxOcJ+W3@L5+~0Q|VS2M(U6BhHN1n{mofljexq+w=}Q9VVg3vtokZF`rNC*#d%N6RSg&GGObNOjk&-kLw5mS_0pH)~|AS?a&E+p6wSU;G=?E zFGskzw)D@(@uw6K$MivVInZ!*rzmbS9ut5iRpT)lqxv`-OxdLB)hp9RaN~RKo#vS^ zrMrZv`GY;HIsPY%plf{HZV0QfwyNOY`3#zkk5>2~&vBmJulX*d)Wv|8A~vU;9%a*P z!I6*r`x4fQ!id)zu0pHJ8zt3<_UlGH+<0$w9ACS2eZ%Ty`&~yfwm&p?bk^U`&$R|_ zr{2ZuU84zpI(c-CETiw|$KEfFyk9sscVsC{PEB%qxz)!hhmTJR^zR-T_{V;Rvb+2F zsB~EAo+IC)_ABSkNzO-DUAVF^=PH=KyZWc^!byyt`&ZbDv(s zV<2?FADKmUOsViQoImz=dyij=?e6KAOym=mF_A$iA1!pK)CUob`={koBHx({6F zd9p0`A=p-42|{lNN4&C3+95@3TyygNn@gMPdf%GArdxz-Bg0?SNy}y#dsYafrvlKI z?X}nVhS-`V<_}u_ktA5=S#Rn-3xaT`zl-*)JmFn?YX1G~3rRYQXFQbCU8ElwAA7eP znRpSnG`%4p=`2j?6~*0Bv}z8IcA9v`ne9jJzT@wnnIz@W5r;syL!aFJwy8s-&%_2M zD(d3V?pK!hc6dye#wDZO@hT3X+($L8Lw`j3cTh@k{xebBfTz|k4w~qGX?tS>OAAl= z(MO}`sx3=9fXfv(m?TDLWR&=f*JqUQ9XlW#7SBL&s@31!JY;Li-iLm{+N|_W{pLrt zA8En<{70&#aYi6w@lKH2yuhoRJ1C0O!HJy&=9{Q>AhFWncv((|X9$4liG+p#we>rT z?hzd_+t`k3B3_dz zHxC;+_Z$KV$IrHma@Nhb#c~kHRWcuwY4C>5|E90_6|dr>FAdnNyYEZ~5H8*u&4fVU zZuIImnERc>{wzZ!?tldaA4JaIxAc?ps*YCl_Mp?v)YJP@Uf?8XV-LF!b!=L?;{$2= z-c{6{{efZ66+Ws4cdXV^E-G-TRM9ly!lpszIN+J^mCjyTps(coklEk!?txq#ugPCP zwB>w&HeElh;J+BlZuepZ;)mBW`n84i)fBGNAY;|q-EUMg4OlurAx;f8y)s+sWj;Sp zMF%UWz+VJT=q2%MIC4ic_5H$l;Mb6L8a^hcBBgKNGttQdiRO{p?%;yn^@_%$bEE?w zOS1Rj@I{|;)L$h$rs}Q^pk`K)TG|Z8N$ryd-vz0Q$ND3TW2QE~B3o!%)_Z$wswOU% zR7_+>-rSc}#`1E>0{LV61sM|-_ntZo_U#YNix%yx75!+>D{`xq9RqdPaB?rZ_V&(B zwB0#8X?Ne;sD(dIQH<8m{Rsf%P(8P>n?c| zUvdv}Rxe!GKycBQ$&Kmc-FWTH`zjwn<|EJ?es}q9n7jHrogrcoF4hp$kgzah(1CXU zffm(WePSQHHDOdy3Sr}&-PF(}SV>3G%O39J8>i{S>3DMObVs-O)6K&Q+ps*APGb`z zZ;rgoPYzF8nWi^%VUyo-de|6vKu%=sM?rMe=QglsL#OBTbpijgdczw)q|9 zR5|(qH!oeU6Tj?zOFDIDmM-@@CGX=cPn?yXx*Q9%$)O(877Hb z#CH0r9Ur1vG4BP~jOxbUweBE9Xjh3vLEStRkG4V>0G}COHIKY5fBOD#`Tf46`-m|h zwrG+cNFi!R8^bo%-oOpKOxC?H{3;n6gZLKrw?b|OatWTHhzI6E{X8qpg?&+;+_m4g z>B9GrOcx!{)4ork7s124LEm7|QcV5a+5x)S@jz@f*e$BKk8+?EmkvrcjfvcM?Fy=V zpE#IXVylicX5Rcl{bEZ8BHAei56C4mw+%_U7y`$3f;8~&NY4I3*s3svGU}20!!S)( z+L$T9^z>)d)jU(6X(56O_^Hr8a1Ao8)6|qELsn*dod8JUF+#r^_Y;g+eZ?Vn3O2qy z*?)+$oQX?_TM=Wt>>NGVoWYHgz>3d-Ze_&*2ZwQU`vllWnUc$4l~Bm0d@L2-?Z7Zj@PxwHus8aKlItka8iHxkcP}Xa||6(vr zkK=HlkGHRD=ziu|%hhAzGw7BG(ao;Kq2u2ieAmTTHSsk8H`7jthu-*hlO*ATtzgTa za>SDs$n+CD+@xp;NORj%8=6zAMi(MbX~Pc z(KkA$wG6v~6W6z6;ht)RRWRPdo+f@N;=f(~_>R5<(T)i*%X6W#QTBj;fqHxS&xAnt z$3emW!^I+&0h~rW9nW5XDDWf=+i8fYOgMXuJy_~hFWZ|#c=n9fagit%D> zAO729m>)}U&q*w?W@UdU(`U7 zszIvKzI_zMBq&=)IRXLX5fPaa<9(7hNxp*c!ifP?7_0Rn9V*U-bi}r6G&;=gGa>HZ`xw-d&F1~eWJ`ZeL0+VsYFl}um2j1 zVv!73$Y4?FLs9nE;$-m1#5s4okdw61xYfH=ym#iUw{Eq#&vHM!RebBMcaqfiP6gA5 zF8~(Bf0pK}Yl@Yl_XneX^8^^3cPjV#_XhWdDnxc?&yIR}2R*pIuT5CSVv$#sH z;Ez^Gc6M%^DV}}HgYf#aW@nGx89rcVDTIFki0yyN<^kDVvS6oYN&4?}?$z&gzwpkP zdo%ZDLxnsvM`Da=aUs*DgVPr??5jIS4sEOMjz%40k3$(@R_#P4=E}CU&f1OqRJGpw z8V5jFrg>^GTJLOxGwS_N-9+Kd0_8*i+C~?(T7z3_p@-64YH9Ypj^3v`N|R+_Sx?RZ z=V&ILP(-WBVx zlb+n94{alCxD&GD?BB-0)@65QR9X5JaH)PT>52KQ@kT(Vezp3JS$E#MvAlMVBO;-2 zZ?0m1nd}%>XjMlSMOp#DGjUA+3^_FSL z@A3D-xqD92t=r5!JGyc2*|$788z~D8v#fqc&&T!WA$Xpo8!l)|S}2|+v5vdTjG`6I z-u*4ezNTA7ZEa>Ph*Fkh_vqF;>fJ9W*#b)s6jj`DN}M>hkx8P($*~kh)#YoiZGf<< zcGG&rzR#z9FN{w7#Fpu}pDKJOU|-)2lwo{ImwAE$Pl^Da4vjUiZ4rCtH$2(2mZ7&M z;ceF)*w}|8Bm0lz$+RFiNNZd42*@TfovDQ@un%?2q~!aA$7FCz*dbBI+6IE|cwdXe z-a92ifxKdG1qlyKz2>)D(_9WMi{>iNr2U%bYfbpWn+xWsAcXXSh8JF%8tJO;?yXtX z9FD?1)^vyOX>mSS+7ClVzm#bH^sBy+kYR8`-zbJ3ui{K09WtJ+zffKH>1DeVRR4KA zSs)0IUNVp|SSD?x3SDav2LJQ{yZBU{ImiBVyds+)rwnuq9E+s?O0HW}5Ba8VkT?7S z2IT-wKyBzPOs(fbtQgKiP)`Y#IWX_Ek4640EjNxHPkE)Rn8zuQKG(M zqNWlI8J1h!PrY^P^Za`Jj`-c-cbDHOoQil#x5kXsP_PL4&IRl>+^d69NJj zP_Zi_qN1Xr*hR&L9TgROLq$YX?AUup@?O8)?jrU>4hFU-nMG-5U$wU58aL?Zow~BdbLW6T!K?Pzr26;p+3UvmR9co_nj3^*h-e* zL>Hv2Fu@ivbMCltc6%l$w8!FXmRlyl&Sq?axyu7wh6qQ{=l%2Hl_}R>Hk)IMQxUI{&dqmEljmoKKMc^>3(sG0Hfjki z$nJ7x&_Dc9#tfdH<-VC~B^P?!n5DM~_vJkA6y1um`-Gjl*w%pCaVG99nz#k2OuHLd z5{-(6cPPWccI!BbsK1V_RZn}%j%u_RKfUS9cRIqGj`Ap%=66JPw~@QI{6@h3THNk8 zvMfh;leu3Po=&px+Ey^UTx3f__^t@vpvv=__C6Io@)T{qqW8l2=ii!V-dCROJlb9z z=H?E|Cwtn_BslhfGeK~B%Uy>jz3ycdJ9f@N9(nZEv-klp5YQwPk?&WA?0f&@22DT`Kp*im1qrb<=r+NN+Zq+?)=7 z8P9j*xckj-@WW?=c2i4yRx_%qY4!dN?$<+=&@88>&%kUZkr`9}c8;)GGi1pw= zlAi&x>*1;h_bH-VWzoWBC8|~}xiS~_#ZU3W;GT;MGs)@8OK}T_=h-_1!tTApt0CNP z;0rRD;g_Cr!uR}=Z@0YzURxQK8<1sJO=w?1|IiuH&&h+HS=o#t?=Z~aIe>i159({5 z5(~JsH$0se8)k;3slR6dU2$ecKYJo0EiZ3iL7{%sPdcZxzRcly*}UAKcW!pr66?$Q zkA^=Ww`Z6hke(HqdiZHadN@qW$R0S{H6iNWb9g`d$pra5vwG&5(|gkr+#|aW+uzhw zZnca>1iMBS_C@=6e$%3*%Ujfe3o&;5=N?nl@^1Il_%8F9(mjjyA3p!7Ep~KJF~;u0 zhtFASFhO4(Te)-rU$D13mbM(QJ1zO0UZ%U_qk`e{5L`y$L?CQ(luomY)?o?nIk0eV z*V~Th_(^|HJ6`nev)XM~>mj<^&GS)q@td@U&vOKJuv;>^;eI%koBR9}BOFeDHiFhU z+8?oe*_W}SL#MF2?dNO~g!K{$FGISWGWlIPMNjJVm~c#MPP!)OGbBOY_&GDevmWiM z-F0KP>n{D}UN{)uL!j?(N7oU8{u8DbaWrq&;Ow!tAS)v(#}zKF9ozAs`m<9wPNveP zMc2jMs21>39kSivY(7QP**!9BBSsa`{j-5qo#vPX^zACS(r z%bQ+1*#6A2PeVnmVtpt~r3v9FSC|#Qopj~W^n;)10-}A_d2H0q-f6Av1`Vgp;bULX zMxk`VhiT@A+rqp0ln%6&-f_FFg7NK{p)qvR-HmVbiieWX*Yt{?yS#rQJf7id8gFg4 zL2&;&Y-}{Y+|p>vCHGhCWLfRVDsz9fbeCUd2s-oP=L#jCd$r%o&PWZ92B{(~x}Qg# znS2Ww*S??|wKb4F;JhA`NAYeNgy+tR`EoT5ELlG@k(#?z(UZ#+qt zcBG~-?K+YUk50ma&S;b9{zgOfx`$U#?MeuT0)#T%qbq_OZg8@pBCdOQyij@=!g4l- zNyGde%^{y1KH1@3c$^gN_rmiNdL9t|PKPTkIyB%{))QJsCo*BxUOL?3M(OC>#0r$8 z!>&q?bO$B;M2qc!J;_)#lZ`H~w$TN+lHUQ>^{Qa|?`IPZ-*mtHgv z>~bMD5v`?mg2TKC?{7wTEW_zua)CXp%9VEYmYn}7Beiez1{ls?N`K6tX~PABa6nuF z4f?5kwEd1A+_Y`1tyy--zw{bNc%wSH?!n0ukM*R^ZP(u3xG_6?<%YdlB7BtquMG&_ zQNTM1X7fS|droE=H|(Zy%XZo{JuS#{sw1OKslA$F>a3BRk4De=@O#?SBL2AXYMI(5 zEXq4WitODr_TrGPJhN%GGQ1na9x)1E5>hm6t_>&bwPkA5XV|;yunzWak?@(p=rtp} zYa}cllqpYE$#cR{=kPh>4#e#;r)3os9n4n}+*{V!AMmlE;k^1{9Q)J)Zrb1+}JnKILgcIZT@sZ*ja1dry$YV6sg-;z_GL8568ST2 zP89U-F?A{f2=dA%gScc^o>mp~@5?XqM;3LZmoU&@qHOw4m>t%`{A9RcJ?*=wBI-$$ zO+5*-OWV`anp0AZjYl}(xNO&+`0TiPI(CR3uds*G>@xLu*>UxB>6EaZ((E$zc-dh+ z;e6pV$R@IzFG|v%-<9Ka=3{%Co)jpZ3gpLS$0f`3S7tm)3x?To$uj+wX?JPCFgq?; z+F!p3(TGKhPf0snw)%6)u)hvAewO2nAmf)Po9RKh)_M=7h% z3P<&Y`Ao;CK9|pIwDKm(hoZb~xkin1&(((8wbEp`UGtKZk0wl>rjJB^bZD8UBTRRo znb#+rOhI0xK%#v3`uHF(QXr8Z?Sk_n1rqs_qv;SSkjRgICz=;2kjS4Hm5&rio*51%kjM{r`<&-S`4aij70NtasC3sW znO|WO@*?rPcGMsF;h^&LN;-*pGGM3FH*_x zk<0Ek*}v)w;QqeQ22`e82eyqfCL}zhV836o7u~TxwxGN|CQ*+CMIG9O2Qc=zExxpO zyj}zzjwF8yin?0o(Ia&zR#4PFEN=s|3m|(0-HXfn7nIxWfLQn9Jz^DOKLlGS^J4H| zNy!_DT^Xj%Vd%#P`TV^h_D%78_@?+C{`QKEiS-XtmgD|SkNqBeL!DK3=n|DUpd^0_ z<;wfFF4!;S)!@}&Q}VXtFM?XJrm@zsJ7TZJeu*_sIwEOw(wRxOCB2&TQ_>#EhbLcK z;p7VIDtumHx4{0zhXKKWFsoL@Rzc^A{VT2wzA9cD9KyfLDlVz?d!>CVe^s1Sd2;0! zQ$9}ls`!WsUlo5no?n&Wr`Y zua(#HYndQuRcSKo=5bX- zd9lH<(XlD9#j({fSIY`&eCZ$5>iUawSVP0u37Sx4mTNmso@Q^&jbHltjH&zQ*k5yrLO(s?xd>woqd=h*Ryi3`Af}w2lrUpg9=GaBS@?cfa zBsM2FIOrHVD|TtDFEwVxCd5|6?vA||#A3e{|NVdG2CoNC#&}aJ^-p8F@louOU`22z z^`98le|Aaz6JyuM9-#iDe^mcM>VH1QA{7Q_OISP!k|BQj*CEeB?MT5f;S1|0jj`_& zjpnx+emyD@D!v}8+Yc4}L`gh0za$z)VkPZ~>f7$BmAB6;TfX>+xO8!u^!(zu^1=Kv z-Gh?u%U!+(L_12q@on8Iy8^PCK5-44XVcXW@zWv2??}8tTsnR_g&PB}Onf_1PrLss zA6L{3$CY>9rUpB^@o;WI1^YvD*bgq7bLwE9Yrc4EiqGbU8(-kF;qg(=`7CXFPLB&S z4m$4TKOVdH`nT`>Y{=UF2Y*-RyFu5Tdc@*4hBdi$=`EEu#)h0$cg^YTn>}$-^?pT1 zubIC6&UfeESaI_eMc-ZV_~R3<_~?vYYra``<>R$pxTbousjWV1eanKPBRAi?YUrl3 zew&qlRJWy**R{Xtnf;SjJw0#Gqd!#JvaZ*KH@mHtoUus_p^^%w+GX(A?ywfz~-mTiz-kR_GX3J z2lfS;`RLxzx}SYu5+!LHL6#uTBUNOiWO3llVU-6<>qv*KR4%$XGXgI%>KG#)4~sI z8IG{DP0QLH3jI!jUB7W*z9Oj)*HFwY>CPo16lK`oROk-#p=4`m<-(!iROk-#p=<#w zW$m`#Sj5UJBP#UZyWtTOpxAcql(dd?@qmfCagtgP!YwaE}E($~~WJI#Dv-+(S)*@@I zwcN(a>b0@4mNy8JlT%VERH#_7Ql-k3t5mV4-D9z&q|#gpMPYuVF=#Z5f{_saemNVG zA8Z;}OWZWKX&OHb{b^=ptsiT-wypDtT9|Kty4xQw)Tvvq{%#F+Z@5RJ#!dFzt7&S- zPMy0P)b-#)4n6GfZbx)a>ye(3*|S>Rwqm_sz-8J1YN#6HhvM-uzQeJ#E41XPkM~ z+2`1Nv-xLJ!{%>{fTBngE3=%3-K;a%RNhX+=e|83Pw`O|CHqZVX8d5&#mbzaOdEHr z*YYB4360{ml53ij?3b*63s%;qorNebOxdCw*uKQ3v4zy|Z?x}p2Ork2Rlw>W)?@w8 z?%%2I*~et=8x?mor!xJ+(&69Y;;@G(gM!xX&;E>vmi$IRwQAL?*QimmX0&uwixzSE z=58NXoKqtQ(T~)vzTwdT0}VJ>#h}-ace3&|_%LOy3dg~kA>8tpOSfmjQZafdX=0F_ z&S!tKv(pMQ!>E04&?0?vb21Cl@&{Qwryn1;%}Tc@Uj&FcN59`Mu)Mr9zDOOGON%=4 zjbFb08+I$q%+Ac~OR;_BBhAeC?M{==RJ?Ptzw4!q!zwJus->F*u@k z-jMNjF8gTkBuB>Aa%K?ydz*8!yMwoaMgQmw;7XOgh~?_X;`zb+U{kP>n4L$w!13&k z|K>A<8<^6oDI0g@@VC!r{!V{?ReQJ&*hen@Dpa(N6_RS!iZ4{%GQw|U#CIt_U878= zlrpLRH;GtMrOH*RYBdR$km@9os+KImHG-516)B^&JQh?)u|BF+tyaB8&04jSQz{f& zdJiJCEK;$0m8!+nifhz#T`ZITVcl$Ft5mOS{ngfXlxhM?HzHEUBC$Pc)n?63PH|l< zlmF_UjYb7NO+rYD#cab-yH4HGK)Ql$$xB1y#@n_vj_tp##$*w*0B1KTv(zOC{8 zVWGA&XH0L)^KsM1ww=ps^z4>RY0tz)Mn8X^JaWt|ew&}l2l4o|$I)(VO1ckGfup&} zM}gArL!t_f=9%fJhPdwG@HwYy%e@_*APVBr%5JSYc)WkBw7u%1OmhR?pQl~Y$Wfzt zulh0LCrmtc(&Q;qd7rahKcURiA$94Dji?;uAp*s<)&vkcc zvck}vzt(-c+K@`z`p=7Nze_(xH!JVbRa+A=ap}sIN$3t1?H5&~c1r!aDK(R-KU=k8wAHw#c;6s3 zSX;bj@N@As#q~MI=u=$&ziTM7wRj_cTT`x4@HH_@T~pj3*a8iMO~uQKzb@WVycr%S z4uTfcKB>5Y_isR;>Pf5*Sg+c$5w;av}{ONQK` zn9nAq@r1uVziB^Sp38@B^^;8dkQQ%Uu)Xv=zVywJM9E@knh`eM>C(qc|B?3jQpv75qYhKe;6F9hVgUS_*r(6(`h|ru z2IZvn%}O6WATzCxb?2+q`JA>y4IrJHZ;xD;B>Boj_{n>^1g5J3U zXw|^}*13OXevjOOu%tcB6lCY7r)6jGvZbsr9*%3@v~046r{!nnr14Pmz@SVo^^JG3-h!34)^NF;VJ5{ zT{+o&FoJnO?YY_ghuha6Ip53aX&J+F^X$y4ASmpcXY;6Vu>IXh=$Bs5scqXJt01>^ z`*vMAb!i)B1pIAXZhnu<1bw6zlv7n{)jJL*9As-=aCq>+9TU`9rgPM=(%PbQZb47p zE7qQWiJDv5wR3{%OS^VS&~-q1@3j2kh1SKsv&VZ%1Aa==wQNbtu zk6YN24ZJhWrtt}?^hPtR+@C?wFw+Y^DVK|Fqo2>l)wuD&Q{GP%ggx{%2)PbJq zw{^dQL>>DTv@W;D#C>G-=-aW&L5b>0&nvG(LAk?llayECCaRKODx~LTWM*d-7H03T zuDQ8fewT3lR2@CCdNG93U{^?On{N-d*j3WjphM{J_}417qD|eS!mmlS<0;SInIiU#+xVd$iCdQpxo_B}=YBy2EwbCt9$!T5#ts z?))aojoN7KwDwwAqjc|D+8^0F1Xt${9mx2rZvW2m)>a>zZZ7S|-FU@cU-jGauXgk6 zaPhyY%a!r%%G;4)BJ9}5<=ZKtjgQ&KjklGv_&=Qot_}41x5EE+{niLK-`u!6`wet? z&eo5MJKOZI;HHtYTTdE{zbfPEOVr=r$+I^5*m~^JR<{doIKHi0qOz9nmy3`8 zRd=f|zPw9Ye=fMZe`;HHtRFXjtUildaE?;ZUt5se5ZH{lBm3713ndAE{ zFYUKuXYu%9y7ri@e%C+Ie<{?f-J*H3z4vO|a5uLnh^mRPlqbjB-P~G1t=hF})v5pu zYwcdER?}K_YL(q%ajIisUO$B>S$srCg2qql5QXaPsy`9_-+Jx2Z64fu zZM6n>Ejqo`n7ym1JyoY2UiHlt{MmT+mG-~Ne)nAZ%CH_MR8cdlRDS;O^H*(tt@2q> zs9d{8<-Hg1N5il5?cAD-1= z-P|*%*4-a|qV9fsW>7x9jNd*R2WxAha(=t5y@|?@U1ogZhh^PueBAiiG_bl7l{-@9 z+;p|+WNjM-7Hqt&9_!yi8cYM1cHP~yvikh_=F*eMw_yES_hT$Uuy)$;tWFELFdE!^ z$sum@eHK{A04r~0EcorWdR<#xd;IaSHYX}$^|oq?u%PqDvSl|->`13uATK~b+}=>_L<%KY7b`FI+zLBa-nSbM}y5L z7p#0bSR1S!w`{w(mA9~Or0@wGt2@5!Hs7kMj42h`$S8=}AyG{`G^)f+*eXGV$}U(} zLZiL*2@J^I%?#e87}m$v>6tk|%^xfNXh3ro7CROnXmlHgzYHzamRa20(HiP^uz zHT2xg<*%z}vH8g^ZT&Uety1!=x~~5|`?zjDBxUWB5vB_Ez9cxluH~<*H>zng!T#yr zQvr}qvrxgwd*-(0c>+TRXb<_U}N3XCALM_NYDAk#jW zoDpPZWTs{Cf(!cZ%h#DR)6&zqUZ2@BtzR~^acSP(e! zNM@A96?gK}a(dA@t(RSVXSBGA-z$9BC^IcPpO#Y{Z7j%5r;&XNdKF|1rZj~LbHgW_ z=olur@~!NgTzyxBLDN^Em(I#aA9rJH(A+A? z4mUUa0+97&FL=RWF?wb(;jb;3x!hyvnVma8GbI`m#v&~zohQf`1*T+XzAc*83)3*& z`mlRR)+kz+Z{yrM-$pIFLr%e<92TYQj=gP}&F+|QOKEma*p)j_h2fmYO)to@!eJTO zPEGu!EdzV@%eMKKo0h|xVtGeJ>qvps)}v>xtyVeqe4Ht-4}(n0U z<|KpGa+XyPCZkysuHme>8Cm^o9)xxaexFQcD}OQv=4IvEG$o#&**~{WN&Lvnbo;yx zv!YLyO@iJ7c;8P!ZjNnNs^3|I!jallxs* z{&W6D`4#ytcoDDHu_60`-G28dJ$B5L8SVT|VY~u=Ylks=E$^hUt?kA9yrdA@3)sRZ zTe~Od9%k{UCZvXmDN~0RO&T-YUXAA+hK5HJ;T~?v)Yf!w{a4-X(V~%K!rI)QDi&c6 z!}$rXqn>3aAY}q$uXK@p->7w5?@`xqJJ0Y(63X zC2ByKh6Pc|)oDYG@mzn!fL*9vbK-B=Fmp=D``*Jx!=krGy01yOvR_T4G!Lx3O zOWMq^1;))+yOD0sz(-FpN8N0J;Gm-9S|3aK4!^swCm?O#cdLpQMNKiwe>dO$j4KqmBrUeFt|z|OV%KsMO^x{ZOn`}C=U0juCnxC`!vd*EJJ4fnzQ@Blmr55dFm2&{oe;W2m|*1{9; zBs>M{;AwaUo`vV&d3XU{gqPrDcm-aC_3#?J4sXB)coR0lTd)bEqn*x!w>Ky`~*M4FYqh;2EW4}unqo%?NAJ1 zh$ZnLvOU1e^ThTjar_TB_TFdTX8SeYzr_J94{X)CP21AvVaxqVhkGn`$k3yQ4Ifed zW47@>{W$l$^A}#Q=)%PpExGuTOP5}D`4!8qyz1)Z*Ic{ey6bPa@ur(^xpn1jx8Jeq z&b#iu=ib%#-T%OY4?XE%~mUH{taZ)|vT<6E2F z-u%wH?`?VigAYIY_>)h!e)jnnUw-xVH{X8u{SQC>^z$#j{`UJH+y2~MJd^LsOb_g< zIMaESdD{4q!I%lh@WsB_k!FN%MB}?-d?q^lD4KnUkQbkr^|P)L?i|&VDVbe5W0ee-U-OE4!Th8WWS5?gPWhO{tEPx3{Ao6ccF!g z;MD&R-XM2~?ySAmgKP5^M*cs+4vpL)qd1x0(^WF!JGt?39c&)h^lws9aL0bP7YEx| zCVqw4CB+okS$*;S+VbJmG2e@a(Z8xMVZTYy@)}={OIIXf%d^>n#Y@JwoPNDuDbEXg z^8Wl)`>a@^ey!aK|F}NaBaZs*_CFZka{Db#KAE-~@nL7<%Zi_%Umm0^C;a972>VIg z@6MKISFfADR#hsL*PnH_Ze~BY@hz`DE1RglM0r+rXZ6MRH=lk^fm7i$SOBNP8E__? z1!u#qW1jyX9`mr0Gi$?b6q~01v*$b`8TU~z8pgm_7zf9|c$fea;aHdic5pKVrouEh z4yMBlD1w(+s|rCmrR(h(B%XUCCt?COGD zdvL+7O&BhWBr?_|usgz;uU)XTU2kz=3=uzV+6cFod-V_XE=`s0FMzrRlT^%qgtUkChdb`8}9Tkl=4YpO2T`t5?h z4V&sRv0Zm{VY-Xp^40-slMDX%k8_!LH2CedVfgj=!}rI@ug@=2UVd|Ln4`RS{B#>c zw-H|Vou&P8@u!bJJiAum!f`HwyOa|zZyR(l<=c7vl$TCa#?P~5-CzD~di(RBr#B3L z{A@k;`?KZB@6Ye%=i9c#-v-*U?e9x$yW@Aa^}z3L>vR^lVDrph7X0PAyyY^!KEJL0 z`sK%WHl3`y3x3?Drwe|2{4(XmZF%#zKk?Jfrqu{nIkxhdUfQ1NA=U&gM%`DtsrU*6yE^q`vy|H?kdueZE?l0SadRu}%xKFFU3 zd9FwNclJ9r9bE9&C4aw`P65B&miEUfepoem%o1c>+|2)U-{erX>LH+U-xEs`L_LV!9REKw{`wLa5RPe zc3avXC#ySA+Rqz78N-D+L`JwoT->%nem5)Yx6{)8{IRq@4dS=2wmlsIE?9YgJ7?oI z!ga*{w$;k|X@5T1ycq#5*fGQim%uiip$`1K-w+Q)BiyyUW7gtvi1I zVA~9TTz1xdXY0n!`v1G*W99sI*?jTSJL_)8G=6!Trha;7%jV9y+p_1^7r)KgS@(4M z|2xZ!ZL9p@*tW_~?@!u=eOv^m{x8Q}_5O9ZzZ);FoIfspypAxFnSv=e;Y{#>X$uxNUn~u(rD3$Fp4~p5(=Cyj-xj-!}ia-#>@rH`iZhx~GB*sYG0ViMm-`R%fDao&KVm8=hT{Nd*^D ziCA4$XQFPdzpivo1s76@xV9wf=ITt;?I8NK;U((khDYZx_{+D&6Q$$x{B!O2xL?+~ z#nG-@^UO(}1zpQnOuhY-- z%UXQ4S8sgWFKgZ6>-D!owjFfAU)TKYfbC0KyF!Zl`xMtbKF{CYTRc&BKhGbAEsHJ; zbrIasi=XJl5AotBdvR-<-wqp|pSJFX3)bCllk1+Sj9<>S;r+m0rV2>sg1`T;JlhuA z^5}wXD--2ezn1TUm9ya-YVxa^p&N7%ZKoxXIQj8-}&Z1C4wwAJaSE$$x!*to{e z&*9{`U~Ts2we9b1njYW^V9T><8lTrOL7q+1_;R-Ih|lYiAkX$A@#VTE$U8Vep0&AM zIr)jk$MO=5ljSEGFUwCfZkC^D{477wI9h(9@wEIz<7)Yd#@E_j-uT8(!$jkmXdDxb zU!rkKG+v3uDbe^O8W$VK^2Wuw#gC8WB^oEoPc&YZpJ?1HKhgMEexh--{6ynv`H9BW z@)M13qIqX!+kgvJ#!uUL`Dq&;KW*dTr>*^d+S=`>t-XHQ+UcjQJvL9`^WxiS>G-x< zUVM8kFTTx|7vFBni*LK-#kb$`;>W@A;>W{HYkxd!do~i{m#a|;@y! z-hc#o0~6#8N|0yQCgR6!*Y+TKZZCei{9oH1#2<(F=anb2+_-R{i{St5xXl9>>|E9G z&qFPqDDCH^QpN>8-yPfAzB7K_+rHCClyCd@MESOlPn2)_`9%4)uTPY3`};)sw$D$L zZ~OiDd~0L;{Iv14{6ynx`H9BY@{L6GCmLTXn`nG3KhgMFexmWU{6ynx`H9BY+LCB| zEkDutT7IJOwR{^!{P#VJMOdY)4e=f=WM^@!ZadwEa-xztsWQbSjtb&j7!_`m*3rv+x)cczx`_i zwx9OXcD&%Htt}(Lj`duyIuqqvTm9=V)>c1lZH-UcajFZ}c0X;y@zZwPV#ii4Soiom z8=v?*8>jfZ!UTENK7Tx|J$~Beub;Nz`{{9{tsfU`{Qb1m@24&9r)`@0X{+B)yYj=x zxAn;d*I#_Tm5%c~owxq|wDs?&t$#ml z)5A~OwD8kbza6)_VDY&>jyT(J3T)5gmB)5yvh zeZd7w=eq>9<9t5(+n3ZZf$nK(tcZA`S>#N<*Xh4^gYm3_+McE zx^;Q+R$e>(dEoy(#g3m{@SjQ8+hyVwUi<(rZoiFp;UpKq`+D)_UfjRu~`t!seZ-4&S_Qr*F zE`sg2)rAf&f;)O~J3e%wvy0#^Ui=_0ZpWA|*m0!`{&L|jFaG@T=Ser$Blef0?p}VH z7x%Z5wk>rb!$q)NKXRd`i(uQXy5MhfvRo#%>q0KrcGHDy7s38ED96js_2RZ|a^WZ! z!T$EjwofkDF`EnhTm<|3K7T(o&?_^@ix2kVL%g`Z>>usr+y37Le|s>(WnzEd<8MDl zdu41N=7L?vb-}icF8KS8@h%hl+tZ0&zU`-6@VCR0T_&F5#cki>!Za7b{(jrvKiGEK z1%G>O#}F>~`;X&YCieGz{=V8jhWEE~Cwl$*+q{##{9Vg=G%^0^a=vT%jHcT^T|Rd$ zZ~r~Z+y7s;fxDKm|DI*+|H3}kU*5_)KJ z=`!;_xh~;fi`cbnMDuai{wtae|E_J$fAU(^u5Ho3cUx56vGJ~Jr_prYwY=?G-ga%@ zqGjN}`5Jq9$LG7YZ_&If@7~a^?c1*H+pg`~uI=0Zxe*z-j8jF4UH zw`Y0mI;w4VY+GW_#@O+VJcj5P2=;_jXa+6dKxhN)p%WYghrr>`9nzsEWI;CM zLO%3^K`<1C!zdUF?oS91nBhB$yAU!I^L_EQAYT30w-7!rMg!QliHo-gaK70(H!I$t2 zd=EdtukZ&HLlP@t1*i_J!tf0JMU3&ONL4+>!* z422Oe2FAm&Fcqf5ESL-P;50ZB&V>u$BDfT;fU98z+z7YA9dI|?2M@uc@B};qFT$(v z2D}CD!29qqd}#451vKqaUOHJ~=sgWaJK>PzVEH5DbB%VK|J2V_*^-2eaS=I2lfZv*27<2p7Tt6kFaf4O5zK}YU>+=hGvORK9~Qwya49T<<*)*7 zgq3g?tcC~R5m*aP!87nYyaelE1H29I!H2LFzJRacJNOZPf#2azh*e^pfGSV}>cZ~O z1onpJa3HjU&Tt4E0qM{S`amuezyKHm!(j|efGIEoX2VHvDx3-D!6LXAE{Ch&TDSpj zf!pD3SPc)tqwpj=3opTIuo2#Y58zYy3Vwj!z`h7vfrF1~PzQF0Jz-xs0NOxD=n98J z59kHia1`{1p)d-@!xSijxiBBjfb(E6EQMvT9Ik_#U?r@Ad*FU}7#@dp@I1T%Z@?ya z4?cp=;A{8+euY0FsS3*>REIjy02)Io><2BOEp&o|p&O(_Z|Dp8FaUeIjunl5WS*@TN z)P~)l5j2HnZ~(M{j?fhjhaS)ivf(J`4?|!CjD?9X6^dXEoD8SH0yq=Sf%9PzTm+ZE zWv~pchHK$QxD{5x-LM)ShPChvyacbqCfEX>z?bkn{0iG4r5f8)s0|IE3G4$cpbd0_ zL!dkKglx!%fiMilz(kk|GvIhQ5$3}JI1A2$MX&^x!ZKJ6*TKzj2iy(!!9(yUJONL` z^Y9A10h{1G_z*sYFW?*a0e*o$pcs-laIXY4pdK`Yy`UMigtpKb4uLetgd-sr3Skh8 zfC(@ej)R#n2Tp=h;B+_}&W8)(Vz>;hglph>xEbz%yWu`~2p)%Z@Ep7Z>tO?If_LEq z_yj(Oui*#y1^$4b2HOUx0=1w%G=wIQ3eBJew1Rff84iJNkOt|H3B4c-`aoaEg`=PV z`oRDg1Vi9x7!D(03>*U!VKPjE888dxz=<#qPKDFqEI1by!iBH|E``hCN>~mn;0CxE zR>JLYC)@+~!GrJ!JO)p|I(QadfR|xCya5~GZFm>nhmYV>_#D21Z{Y{{8GeIpPz*^m znWj(~szFVt1NC8dXasvgD(nl*;Q(j_ZJ`5nhOTfZbb~a=fL?GU^o2Yqfc`KDhQe?d z1!G}691Bz6I4FYSVJ@5m^Wii&1I~u?-~w0-7sFDx02Kz%x zXbtV4BXog-;V?J?dO#-hhCYx3M?oPBfWdGyjDXQF4ko}Pm&H@C2-bXW<2S8P>xauo2#dcj0~b z2tI|+;VbwSet@6hH`oTnkW`2DA1XsNs0nqTKI{&SU{6SeeW5uV0Ii@cbb!v#6%K`N zkOmph3yy@okOu|O9|plt7!IRgER2U^VG0}vMQ}XKg_B@DoCasW*>E0Q0E^*bSPECb zRd5Yl2RFhka2u?GyJ0mv01v~Xuoj+zXW)5w30{TQ;Z4{C@4y!L5I%v=;7j-hzK5US zSNH?AL#!_AKU9LMPy=d1J!k-XKoe*R`@nwC0uF>W&>lL$L2w8h4&5OgdO{XtLoVb) zKNtu@U>J;qF>nk_gvl@sX22|%11G{fI2BHZv*27<2p7T zs1CKDF6;&kp)u?QdqXqWA6i0dXa^mk3mgoG!4c2{GNCv0fgCsr3Sj^YhNEEwjD~SA z0Vcszm<}^xHk<$_!zr);&V+N|d{_h*!6k4REQ71zTDTr=f?MHsxD)Py``|%%1RjGY zU>!UQFTl&N9^QbB@HV^)@54v%DSQrJ!ME@O{0zUrHYkQ9F78%<%1{kzLLI0NyF(+` z6H;McXbuNJD`*QHpfhxZL!ld_K?d}KBcU(kK>_rKK`<1C!zdUF?oS91nBh zB$yAU!5MHioCg=cVz?NV!WD28Tm#p^jc^Ox2CLw1SPc)r!|*7qg{R;dcphGYSK)Pd z6E?v+umwJZPvA57625`&;V1YN{($We+l}=fDnV7K0kxqXG=M#z2{eU$U_WR92SOWY z51rs3I0O!d?vM^WAq%o07xJMW41^&t3`W8jI0h!dWS9msU>3}Q6JZ{l3a7(aa4sx_ z3tQ+d4jcuAFaQR_(J%r=!#J1#lVB=LhnX-NPJol)6j%Uf!Z~n0EP{*R61WVO!PRgr zTn{(Ft#CWs3HQK#@E|+_kHHhL4xWV<;AL13Z@@-)8{UQY;UoAIK8LU1TlfKfhTmWt zSXdtZWuD5n%D2h4%lFFn%MZzRpT)u>k%BeyI@#{ASa>f|cz?yB9qfJpp`&c~JuKKg z4+{nIJo#za?mt*~R(?*ldlD92kiV1d9)yJ-w=hF4l4r_x?cBogvRy~FaDsfIe3E>!Y}eW?ERau^&yejJyoIymv*mN-^W=rH zU8}dSNWM^BEZcQ_3rpmS4S$;=;SAI|4BHKM43m?cI${)#g|Hr~7@~84v z+3pQl_+0)%{!+I4Mi#!7zmdO{zmvb0e~^Eaf0BQef02Kc?Ov3H-{n8#ZStS;cDY!# zdsh~cH)f%ZTvxXHWftnoyU7h?yO(C6 zp}dFONVfZI7MjR=%6rL8nt>v_m^A92gr6$&%%LnE4j7YMz;HY z7TU?}Kd!)3e2XyFLCyPPKXknKLCg$y}U?kV?@ zd&^n!k+R*tw2&?Lm2>1=`6xMGE|3dlyZ>pSzdS%5C=Zec%R}U$vfVeeFkGG}A1m8E zR0}iYB6+5Ko_xM+_f;)iC@+>Tl9$LA%a_QP%9qPm$XCf%%h$>)WV>f;;d=Q7`9|69 z=UTW~zD2%OUMb%u-!9)F-zDEI-y`2EKPW#W+r457kH~A}N9D(4yPs@ft^B@@{fNc@Mdf+*ocR?Q9XAvcm6%T44x<-O&7gB zasuz$Rp)Z@@RRCJXRhjA0v;KC&&}!W93QmWO<4_Rh}jvCr_7W$VKu@ zd6s;hQ$ydr($ydwE`3`xNe5ZVue7Ag$e6PG(zE8eien5Uuen@^;eneg)KPo>aKQ6D8pOBxF zpOV+fPs`89&&toq&&w~!FUl{;FUzmUugdG?*W}mbH{=cSoAO5aEqRmtw!B$>M}Aj+ zPu?QGFMl9^D1Rh>EPo<@DsPoPlRuZkhu7KkChZq(f2+&2<@$0%xry9NZY6h;50gKU zi&{qY&Xi}#;S1d9w7gx^{iqI+^W_4$Q0^!9mj}oLTl9$LA%a_QP%1hTNH@^bka z`C55}e4Tu~e1m+We3N{$e2aXmyi&eRzFodUUM1ft-zDEI-y`2Eua@tV@0TBtACw=G zAC@1H*T|2`Yvm{8C*`N)b@J2lGxD?YbMo`@3-XKdOY+O|EAp%Idigc^b@>f>gZ!qv zQGQF_B)=_hmfw+sKce*^CMU_sa*AA0K0pq(t3NqOPL@;T3UWocl3ZD?B3G5G$<^f= za!t9G+*i(#bLBiaUoMpU$^GR4@*sJzJVYKU50^*CBjr)@XnBl0RvsshmnX;*|XRrgEyhx4e(MuiQ-DPi`*nFSn2nkXy^tl9S~Wxq@6#t|V8MtH@R5YI1eC zhFnvwCD)eg$aUpxq-a9+)&;_ZX`FBo5*|0d&y1ZRC#ZCA9-K7nY^FeT;5-9 zAs-;Oln<0!$*tu!a$C8b++OY=ca%HHo#ig_L2_65VEGXFQ28+VaJid&gxpqGCe2hF^o*++@ zkCi9MljSM$RC$_woIG8gAs5LrF6V)-I@iF~nqiF~QNRK85UT)skHCSNIEC0{Ks zm#>kpl~>5u$=Ay_$T!M2$v4Zl$hXQX<=f=j>MgCR(P5xc}L*6F;DQ}mH5lpa$h+|&Xx1zqvU+KKrWQ~$^GR4@<4fz zJXjtg50#IWhsnd`5%NfRlssA8FG<4 zQ=TOsFVB|e$aCcrAFP1Nom&g~(m&ljOOXbVt%jGNNW%8BsRr1yHa`_thT6u;1(wotC@n!iH`Bizn z{F?l_{D!KkL6F~Pvx!hXY%Lr7xI_# zSMt~LH}bdgck=i05Au)lPx8<5FY>SQZ}RW*AM!T&PkFmsEQdQO{Ykf|amhAR! z+z$`J!>|S(gSGG^tb=FZId}nHf>&TYybc>+BW!}r@GfkD58xyC1h&HG@Fjc=-@^Cs zBm4}%!tbyRwnM;bY458`fr?NWszP6p#`*rR?r68 zK?mprU7#x*0*662=ng#~1A0PlI1;iU2l5~v3ZXv?guyTrhQSCJ1!G_wjE9LZ38ug_ zm<~lS3uePyI1x^U`EV*MfHUAMI0w#yg|G+~!xFdzmcr$*46cIZa4lR1H^5DB3#^3O zVHMm3_rPknA0C82Y=zI^ zOZXbTh40}<_!)kM-(eeUhaidnkOCFK-n~~9?EQN+!QR1F7wkQJ4WJ>|yZD;GUXTj= zKr?6#Ex_K(*9z?2eC?nEbb>C>6%K*Jpc{0D9*_Y&p*I`}*^mQykPn5>9|po;7z)E+ z1dM_)Fb>AUM3@9qU>Zz^BA5lUVJ@5qC&PR=6&Ao5a2A{c=fOf)1dCw_Tmnnsa##je z!E(43u7exkCb$Jw!tJmM?t*(@HQWym!o#oz9)q>;B&>sH;5m2!UV>L(J-iMZU?XgT z&G0U4fe+v#_yo4X=kO(b4d24|@FV;TzrydZ4Yos&%>0KGs0fvzDpZG>P#fw(eP{p; zp%FBJy&x6#fo9MgT0l!^1#O@mbbwCK1-il^a2Rxh?$84=peOW(BOx1dAP@4P5cs~XFb2lKc$f&2U56x53Gj!;X!y9*1%)17M_H4@C-Z$FThLi3ap3M zVFPT0O|Ti>g)Q&_d<37sR`?vggsd9k#)C2vV5;kOCEM%X)qm%U>3}Vxo{$!4D;bsSO90hS#S=V2Mb{l zEQTd;2`q)nVHsQn%i&tM4sL*(;1*a3x5Fy93+{o{a6dc<55pRG4A#PvunwMq=imi+ z30{Hq@H%XOjj#ze!@IBrK7fzl6W9u$! zu=ht+|Nq!~6R?=S|L?!1MNMh3R+2=75Rwqpv?r;gk|=FbskBLwkS$TtE~!*P2+5X& zBneU4R3=MOmJqTv_w&4GCd23R`Tlhu&w0Jh>zwylUT1mFoPY!% zXGjhLL%=W~1ISsD@<0J70wpjOi~}lQB2WcWfEpm@OR576pb6#xEifPG00z(phJc(o zX$s7MIj{g$z#7;9J75nSzzX05oPjHF2Ohu^cmp5c2mC=G2nHb_6oi3r5COJ;NU$C3 z1W_Ow>;&(K^3?Ms=-503+g}vXadck6)?e5@Eo**4$ui+gSVg?d;mS* zGw221zz@&|_;8*k4G02ZAOh$>97q5uK+X;w0)_z@AP2}fq6$C}D1otnoGGdTCIVG3 z1(5Sarvr7M0W<+QYg7x&2ReWO^noET2ByFam;(!71+0M$umkqM0jvN{z!|s#ci;g$ zfj960e!w3Df?yB=LO~b^2N7Tkhy>fgP7np6!Cnvx;y^q|1j!&3q=9sh0kS|g$OU=e z7{~`FK>;`e3PBOL04{-IPy$N9bx;P%K?SG;Rp1_|1`k0kr~?h42{eONzywdhbI=Yt zKqq(&-hyuM0rY^+pci}tKR_QK3w|091j0ZB(1AFR08(HO7y^a?86XGbfdWtjN?ttl`A=D-4w zvu>?{4X^|DfSh}~0yqI@;0nkYxE{b0cmp3m&cpQwfgl)!0CF~N7zhUuU<-%@+rdr{ z1){-T5DVf!JV*q|AQhy6bdUkEKsLw)dEgkx2PZ)RI0Fhn5x4*@fnrbsO2Ktd2FgJN zs03Bu9;gNnK`p2Q4WJ1$gI2%8d;>o~AK;^-{(&GM zXZVT$IuHjEfSl($2n+$kfD9mK`^p0apa_%zIp=pAPyrKxDj;Y6s)6Z19cTb@{_h;1 z1?B@CzySKd5EuhfK+Xj=2Nu8zSOXhi2ke0ZSOJ`XGjIj&zyo*!Z{P#`fIkQX!5{>L zf-n#cBES|93ATfsAPPi-y&x9Efq0Mzl0hm+1L+_GWPxmu3-Z7*kPl9R0&oTtf+BDM zTmr?Q1eAj7pbV6Q3Q!5Ez&%h69)em>2O2;VXa=o-37&%IpdEC8PVgGM1>N8S=mDQW zFZc$2fIh&7^MGl9oO3J;L;yMOSR6H#gn}>-4kEx75DB(}ogfNCgS{XY#DRE_2$Df6 zNCW9017v}0kPGs_F^~^Vf&y>`6oMjf0bBybpahhH>!1vjg9=ays=z%^4IYA8PzM@7 z6KDplfC-+0=b#;QfKKolyanCh1Ly&tK`;0QetDU5cL10UcA{6QcH1|c96gn@7n0k(ihupR6KQ6L)Z z1+gFw#DheT3{pWFNCz1p3uJ>_kOz)|d~gyJfHR;F6oCui5-0{GpcGsOWuP2XfJ#sW z?tyCX5Y&P?&;Xi1GiU`&@Dw};?Vtm6g4f_J=msA^4hNOq)G>_W#D!|3;Mm z@5!;V`P=`TZE8Nwb|oL#w&FszF#Z$7(Es1@Ngjx+B`;U@8?{eIE~Un^12V|N2fTC` zyfiXOq}f9u}g5wttben7iAMyq%6sc`&l05ZiSFK)O7uBQWTd5|2Emls$CkTL4TzG<9Yh)?AC*C2Lq zzdiDdg*MSJd5*giUi4d?_r$J-kkS%7>)&0 z*&>WQdn2#$GvVt1d3Hly$2srjp72{>HRpz`IqwJX;4Yawy!+$Dy?<^nw_R{QbK3>? zbALPN58&2oe*oX+clq0v>8_bf_Wk~R@sIMn>iS00=S|X)rk=0buLKs6+WF@f|NMk0 zqA5=VergVtoccU)%|Ir11ulJ8t{vXyg8z#i~CM7OUikfqwQBL8;V4*TY zBir`)Yr(rJ)}IT~pBvDtay4`0vC%d2{W|X}D=1BsJ2U0GW%2SwZHf7B4qrWKn>Ky% z+8-|ygn!%^D|>X?3Kz$(%)PN^o}W0re`0|9V25=k@e<2QR_CA63)ozoQeZgph=Fm^ z>MVhPg6P4c#G;a-jz>3YKD<^tRNaN!X8%Vw_J1fcf4A%e|4GX}EgK!?v;EL@g&pSd zDLdtE4xM?EK15DoqnT`jYL$fk1Btil?z_Hww{0BtSx;f()_aH2CXF0(YsvgE^-j%h zyGL90s6TL!-@er4_Myv@;-{QPCC+6Gsl9=r%LJc|ERoAmYKW39Q@U%)0Z^f98Jtl^G6ogyzG)xxX448PO{fKx{?zMP7 z8KZY5j~_16_KvF+bv|h_TW@mt;U4w)8kj@qrUFY`stk~NQ}1gzWglkvc!^UR*}mg%bM$;C|; z74PeOwhvNOm)&8qKJNPJ-iM}B_PNLWytQ_6GUYCo9m4-ZaPkpXo%GBIVz{6C>e14w5@0E z7pr>b<|8{;1_tp?g6=v~v#+gNJ}dwByL$JI?gYcYZPAR)iC#Z4H-zo0m(}XtpxZTL z*}gABZ5>Z~$NUr-@OW>&^6B&K4k61nm)kt%HL$OSK! ztxmbW;Cg!N?0dljH=i!?d;5CHl=V)%j}{zU^XdEco!__GzZ-m0FGeSO*9>jF^)=rd zdaCRUdnfnK-{hbx(=l{HkHfs(;~n)qL(e_#_2Y}Fd@axrr5@IC#vm?6z&om?gxPxg zmd(a|i6P%=ChCZ^GQ(B{eR)K`@?JryB<|vhF;2|9GwGi7+Ar#AB_$hV50871KjB&Q!$8y4M~j=!oh2I^|Gxe$0Zs|< zm#&()$K1d6WoX~tScTNy;kqL4tSy<=mX}Wtp3vcC;eUF*-caN9M z3sm+zRo{NLGD2d?kn#HxR!SR>QRPqK+p*>Af;*9E!+N8q3hk_IZ}C_;d~mUls2EE( z?YQ;WGlPT8)#T+i4C|X~G+ACht7Kl$CNVHPzP_?6(+&TybG&jAmbR z#u;-1(Tkcp5?_@`Nmn&a62F@?UG24_+iB$iCp#oWd)~@!6nNtsJ=^O;>RgNL_}hBJ z)z6(t$_=`9(IP)?*&Qt*XC`Z9()2urRp*n(m0P>MoH+P$^;<=);U08_(2Jw}z6GiU z1|RVr{yFOVx~>D6uUp?=iW_v^<<#zqnXS<=UD>Z5&1%bVE!63_`(VeFxp54ZbIje3 zvE!x;YhEEb;!wrXeYLUF)WL=1K9{wOO;GrhKaipT2qY_zN3+6h+2PW=fSbOgPp`*^pKJJX$TRE5!w$iD zcLqPX?YYH&TG*cSyUd0kc}ej%&NS)b`7ctm@2X=@u1(HMZ1+^jY}C zgb0cXFUx)~;=KB~*V^I+s%LFAZzwk1f0cW$XEtreS`*`h$Elf7Z{iB`a`O`!z3(ox z7&GGj#M2$$qt~sHtD4iLx%Kem8>c-7eb3X;&M^G6?9EuCd#`>5nwmAJWd=-^NNl`T z*Ok@fdptgD?ik+)r$HtGsT=n0i;Z58@-_M4IgO5)tkyM0ryY<^EN&d=wMVj&;Nm~rmK z3E3E_;$6F?tZ!%Qx=fnO+%#{4-rhI)Nq+8icdm;pQC(du;nqE6k;B!`i(;Q-ZA(dS zIdN$A!-f^p?d+5<%D;G4`@Tmqb#keVV6S=f;HZpi75qO3uMK|K-lKl~NL4WZ!4LE5 zt9O0dz3{HoBI`5E;gdEx(9fInZU3pOJ?qKk1pU!JZ95_#4X)uU`H?87Z*(bp%k9bz zpY-b|HT_=ikj>%qn>*v}N!91>EM>2sQFA{R3N-Iqf8|!=1$%Rk+9}?0fyM*lg-@Bd zG*7)7KCi$xCGgePRU!GKcf7VTJ=jvrH@Ri6aI5Xi-ley6FV@=3{vc|7B(p(l&~Ztr zqNk%b%%AJLXG+$=gt&E%hl_Ir#1(A|f6RPdUNl1GqWKqVvE}ndkF46^E9h#X!#F7u zbjG70vGC#U9+xHZpBmJ!`@Qn$WsMs8HS76?lW%88k5_7xygc1RS-JEOJ=gW_0x1Lj z32M&&dh35n;NKGXw**iE{1NwE$62W#df{T?y4YgRh6%^IO{P{DTny=v)Vts{cRzh{ zuVXF$P1nHpmjlkWT}v0}_&#@+F#oH)180AU*z-%I(8W8;)5BOh zl5eF{SlU%9y6SCy)cJ0c^*R4@4^OA`HiQmXcD2Rfjga2lTXNf<{rtFda*3YTykm;f zXA6zhe3777C!Z*uxw7p+#*XDV36|II*Sjh~H+%nqs+)ROht%ZDJx~5H@)^ebwPADyBkFoN?kXbrYRGYrV z?eSEsxAn}|6-$`DJ+#}od&ynT>dGlEpB!#4*nL|)W@B*L>Cr+(4^B-CDjBx1L#4j$ zp!n{n7iyaP<+9hZrrPXVq*bAFz%u5Coy7>hkmE*aH&0mS&~EywoRu`|HgfkFP8})H|mB{(GPv;e_+O4%=(WKHz@19*9(p0Ly zzpP|i@v-#wvZCCmdsBus&tF=R{oHLh6T4y58f^jKG&c;QR>H`BbLEK&(NFdejHKv+?roiJx{~lXWAPR zCzCV9N&*{?X13ZN`*3&h9^2W|j>?U^?ItAf=1%g1@pSX)QS&b>S69|k=@@f0wp9JJ zLs1jG#nK|3uW99p-uhgPaN8BPB9FDRu1s0{=$wSdp27JokykPU9Zetbv8ujBFJ2P8 zJH9|V!pe9`ufenWP{DMT-%FqFl69S{HLgjnQXV!ne_Z@_Z;v6uu07gww!Cp+7#*-X z8)~m;dt$8iXs zdF_4C|int#%y54X2z@HM{> zNS|r#S8#u}mB^^f1I|HXdUtGBPHT}mcR1$iEvx(03TM|d4W`%h9%9|ne0#}x$;S5= z&(glQY%{A`KECqKgGM1{{^Q{t^tf^%i)Z(oOFKtOUNm9K1|C|vbXaKC8T*m@x(9aO z+vwgRpkb-BWY)}Co0FQ9G8~7+M-IGhTkQeUECQrYjGLLqT%cd7 zW!QU8!;&>@nmc`>s_m@qCRf3lV|%Wi{vu<$DLj3??Q^i00unXzi66AM~e`tG&_-c)b6vE58Fg(=v$HpjTdF~hN| z*EDw>J6&)<(z^6+?Y1X9p{KUnM()sj@uG6y#3#~Qaz2+G-eoiN*yrdu$wEi0rd~bN z-S<+}*|fvhZSh=FBRQ2`f03x#&&^{Z=cmPWn^>3%ju`Ozl}1(T@DbwA6bGyPFz}RR z%=mi#l;Nyr{O+^$gVY|xN4y`TySwJWK;4xZUh5kg7H(`5D}H6P&oUyS>1pK9YnspR zz8||hA#-V>&S?wL5sogR_9cC%-7flDucVhQw_3#%ajAW}OD9|CWvyu354~}_uTB4! zW2C=Wdi<&8%HbLhyUP}CKRadhyJ4B0#ru}Ji8bpACnXqKyCyCv?~qIjlpEN*KtZ$S z$jXTmZZnO`?EVg#Hn|M2$q6WNqZS^Bdn_e%Dx+g%=ZOyF4e_Qw?^53Nr!`6phC z>3PJF9K)9V`dw))u~i~_+pzr+H~EjxouH%eEwa9H)02^-BAvE*ol$yuZ}XK#OEUmhCP04DfX@IUCzFgU1~O0Buk=)JqJzsa%X+vVC0lRxzpN72hl;*z_hmUanB#p>ZN^!$aft^J=$KqRz=a z3v4|+{OH}IZ)f`4@p-T?^wFybGwlJ93-gv84J^ISO6%Gd`Ox2G{m{FwhTA^Ld$Z15 z%jkHU+r<-0SD#=lwJBW|p;_lMo;LQo*tQ{xPg)!w#2M88-1t%R+?FYO=lISJ7}EPD z=ksW}=UQ?z%fwx`mM9*p8c5l(cPjbUN&%Knj_r80m{(-H^q#w(>A1-{I>Dnjb zIof~EIz=T%5!3hn(gz9}j%#+$)gN`gJZ$;6vX=#?n^gDAHuPw|zp+N>Wl{TsQ>I&` zRW$}k`!$?T&i@>~Q2NB!c-K11bM&(3m|U?cSv-j$2nb} z5kK#^ybX%(l$Tv_BGIaz-Vi_LK+`#eH-Z^5jeYAKBb3$BHPZpaotDY@XuVQ1gh$3>(E3Ji{#VOaa2qS@)u10l!H zU)z-5ie(E9UK_osG`weL>Ze0?D>b6kGCeFhWAH5o*2WHD-Iq&;HXpC8eICc;4~rf4 zwmoEn{hi};UNX;n4cVnK>%>hxuSFw&nEc%JX@cb9xPTfP`@pj6Kk}d6y5mrPcUbis zri*=kP1TlnJ|d;ME0h&BO&S$$ z;F+UQk>VUu<>F8tSedoIZ<|Bs2-~+SmG@dyzdtwmX#3C8DH|>vokhR6Xn^FnFF$YE zv68~y+8BPjc`I`cEl(%oQB%kYzqM?YW8obgD$bKtO5`3n{g z`P$p?V`S`ykMvQy{pJc!*!d*WHa>IWj+mMIFKbAst~w|BU{{0ZobqsgceVYtwMDZ% zj?dmAtUTr2sL+8WGm9)4N=rTqExFPyvCl(Az_o~>n*V)hvUJ@jqqJG(n-8itY;0PV zm2Gxmd-B0&7x}Z=!t8I~nRasert0mdV%}{HUglNwMAFDE;J~BAT{~A=ei7GcE|=W< z!%t)NDVx0+qU{?4_O)&K9H<>9AhG6V)TY||e1WOYh4yN7cA2?Kn`=hTdwymGlYXvo zzL?~gm3-N=)(&=6mrXV2)724bAK~1%Bx}O{%i-z{Bf9Pcd8tRXFL^5&xj4!5(q|3L zo(;<^oI>JEXDl~b^K+%q#pTC>0>(MDNnLYUvxp|;etU|n*@2@yw})Pg{TV&OFmFn? zsN9Q-r(@zy?i~5-+vxN2Z9AZ>HnjVd3B4ldsruIC1Lm1ZyNn#_deqZnC;+|GYrps@IZ z(A~J)2ZK(1RC}uwDAlBS*~s;ch>gEojnaox*1lSyHmOI72knvAaMQ_9{(GIe&mlQu z-?ZBY`?fsZ*_)AW-0-cmFmiKNYw6Z)p-DSB)K0!GmXE$;Y&>$-1E$1}0_kd#pXcIh zH|{Zd`C-+(6VAu;C$<;TKW+Rma zR8D-gO`=37x=B51m9M(9;_3;n4tz>2GLjvUc~wGb>(AJ$udWS!eLAI1TfO@Xt(xEO zSZDD4b5ZL`^WFqlJ{2iAU;doWe4+Mo3pw?cltJbP}sO&B@j)44O19tkRa3c|O`THT>w3&%+fii-cN@ZXPuJg#QE&n}-a(_a{oK5;vGs zGE)?`Z_9}qXz{t=ZPSe#&Ea1>Ej*ICmX|mk3_Bmv_$nd%?5udP6YGLD1v1JQB?I~% zI<7goJlbsI^_YQ@kETkFak?+AEo8Io3cvJRVUrr0@s)k07MZl*>^(cntV>dLX9z#A zd84@U))?K>dxF|+ih^Sl0?jWbJsj;@l6X+hSz~;<&Vhxt2HWz7`YvgGRQvCP%y zH9bnqe)73ix=Ln54P7wmq?Ka2T~3u?di#p+X0Ay6D#>TBnBJrxdKR9Y|H zu>NeZ#N0_Ue#A!~U%Weuf4O(*L_NO7%6*-iL<-c_h&V>Nnk@ERztQ9Q&sBXpdu_Z0 znd?RypOn6ne9usPX(XS1*Qf(oBYdQXO=YFc*ZvY+xpbP$(nxi~H{CYV#UGsK_sneE zm%dDA?EJS{xzA_sC`>#%ac#&5?VroHiXJi!jW#_WH*|B@6QNn!dn5urjLKC?JmWE| zCQUgvlBM@5=Kkib4L&yIn;6C_@wa9!{r>R&?&$EN4JYYAV=7ywzZ2Pf>?`Yg;<4G) znL9t-ea095(&(|a*WHNJi7%~2TcvJw&UhnbGyGlVeZSnY_Cqt?e*8Xp?x*_eZ zMV>H%HFK)feFOE4z3Ybyl$qV$_4)Z)r~30*g-pkx>*akGYuDadyYc&c;l@3}j*drW z`mLMnHo#Ca$lEQ`b?^P_8oPrQj^6*ycz?()q30jZ2PTS%*-FJ+`gW!MVE4UI<0c&~ z8ZAFy)P=4)-fzbD)I}@l-}imt=2WI(@$TTh6T2gSR^{3~Sb4a4e9%(Ck7hv@+bl$X z4m@(~lg`Y?p|&&4=UqCt$U9)>A&>8Ow|wh(S2^%_(#NS+EYtdqtn-c>Fm3bhmGiQb zjucxauJ}9&M%)6wy{)1b{g847KCa4T?`5IoeWMrt3 zm)H|4nYO!Ea$4@4m7sU;maFvlKc{O|^^;a`HhCw@dFPHY|HU0=CsZ!DYVdAU_^xRU zA5M+8mJ~{VqP4B#Qc%9I!)LxDYt!;q3^{z}l1uB=Wy-KHh^Q@+5Bz%g?o!S?>pu`ve_`~%%MqrcfD@(-8M7J zlboJ-H*(vc`4{ruh$`p2-nXIcR(NxXL2=J?ooh#SwI3)IJ2T~n!V{L`)^|gedMB1Q z>{)kru4I?v&~>$ADt0WWv&d1hO`f*XdAFSXwD#e9T|*CsFHo76Jt?xXa#gaxsKBNC z?mkD`hRrw7YMpn|W`52)de^P_A7^U^j0_6tU9;KXi{C2eJr9T9>DiGUd&x6zzskqv zYfsG-zfjr@=Vr^?v_O_rIhxzO7k9`Iq!A>G-f8^)~`Ftze8 zb1LxJuTc8<^h~J<)(exjZaWe1S+O{pIn?CLs9XVKyZYi~ZRc8c>M)eA*UAaXs6Ox? zoKv{^(iv@)6s5%L(sw`BH7k#KVx7Mt=kVB(K5grk>E1nCy6xb_TkkfQJhPU5wfnAH zz%f3{b6qLz61n3=f3yakeJ6FaGHRYg;Z|Wj?WSEBvD<=1+$&dPeYXDiCF|#ByW&6I z=3ghYcD3)?4Ev_j+IO1zoEWj^wGRhPo{7fU@5DzQ+T1$w zs?}hp+2XSEC{-dST-9*`PtV6XE0yl~F@1=j`aoO}JJ zc~}RSS4dBa(%v=c!Nz@pj5lJtS4Ss(U!1&Q-L%BpZAnE!F*HZ1%>tH7x|hU0Tk=$` z=vwtth3T;uAN2gp)4#TmWuqo%#Z1{X#9dxG&&uQWQISu!o8wJqFL=MOIb;8n*YP&r zJbl(}y&fa@ZR!_anVDthcMkdLyGdY=!t0d0@#}(&2Nm(_Zk%=hf`@rd&T1js!ipkN_IrVg^FOOR`l#U4vdF#{<}Ipm%5xTb=qEDgo-kRX z5uws27b!0r%Y5JF82f$c3Crx;)ko*1O?hndL3V6*#>>&Ka{&*CA$@ygEyz0R;ab}0Ac$Pb82 z*N&8Soolly#xW=7w6o&qVjGX97Y}5kKlqj_vp{XmWW)zKGGd zDWj!W;|C37*mm_ET}2_QGc5>!H`DzY|k98NOR^lhUhFJK61>6Z7Si?UV)E zE^FlUWv)sbIb~zljE78sgadwc_ z`8IEDYu4G!%Nt{&Joh-wns3t)pd&Qnu*m?X>&42eU0;tm>YlP%$v!ddzEgq z+O#W8_qaK3nE0wq=_2t{t!FWG;vX8Ry(pWipf4Mlzei>FSA)1suL@5c8R#}N>PzcH z5!rWw(^{Ri1aHn`(Ps+Z4c|@gczHt7iCMLI??Prz*JjCQJ3f7xIBP>jbj)O5`LBnl-H>Z^@JF)BP2$}H$w-hWz zrx%Srwte)=`g7Z4o~LQAHQY6Rp2@hI+Xp*LeE6vB$3f8sy#P&DdeMvpmtH>U zi?xgo@{}?ue*a?CiD$>$k7U-beI+3{BujB$;Mt))ojaNpvoruP8jhUvNzJ=rlh~aWvusV? z&zHA0yFNOSYq7lW>d?)LQx`3-wJjaXcX|=uA-;^34EYs-l8pQ-nwRen&8_X05x6QF z`EHJc;)Z#5<|}v)YneLU?$G^ja}Do(Dpi@8=`!Ql($45g#SAa$mSgkmFV8d5wtCUI zWK*ERa3#_2ijQ0))BU_hB-|T7EBiTfO_xZ}x4eY1+42tx+~!T&+#RlOJ5HhK$VdJ$ zUxvDfUcPzcu)%_CfulR8>hbN66_IrxVcvDceZ%FomSfj^+M?Skc4gN<@7A6jPEiBD zEZept)g&-WgzkUKbhu7pZGKB*LA3GNh58$3_bm(DP`%;KM%j%v2Ym*KTzvMvPj6p7 zjd^j+S4U}nCHm)z+bfr0ks*3S-}H1qTBdl?dacOz%SWF@U(;UkR!~s<;HQPb*RM;Y zXkDr{l`QEH+UT0{V!6h_Yq1l=7v?t}eKGk+L{ZC&r)H~Y-fNa!|0>K3ak{d(`Nuik zZ;bksF;!KiC-#-AHrS88Q4r8s@IpE#mzmO25c+g+mX3boXyqUyeGQ*fky+BmN2=|P zIm{rZ2l4Uw`n$UM^K8xq`mbKY`AGd@Y)z1N;A(aNO$zV+Zv8Q6+~3R*v73+UO7GPx z1KfN9+1gZ)TcC%V|L;Moct5ivdiVwe@+c8oK5oB-aDsk|3oSo?-#}lUm?UT&&u6xI ze?OPs!k`u4<{!)(MuJxH22i;V@cTVzm0KWnkIw6TR<3b#B*n%Jf+F>q9ptjo$JeJ{ zhtlGf1dkSVH{8$ch^zcpt#PO$f@b_4 zkp!v#HRv~6Jhw=0o1_@%>%&%I=N^^D&K4&ST2v6R{#y{QDPjv+zZC`v3iKr@@S3M; z`}eZv1_iBgcJqfWB68aVHzo-Ra`s;B;^np;*(5I#M4Aa#g&Tx?{uvbXXLX=4@nnIr z#SY?TfgALjeWV!R=Hl-b_**7OE95msX^|j5Uw=;KI6=I|NDyhOzh^aI)n9T?HQzt9 z{C?M3?Xwys?;GS7;OM&EhbkP@1*xy;LC75yz{eNBY4=E!6GDQxEt)6jxBiK6Xpu_f ziAREv0&fT>=(o7g;?^UN`9J$MwEpbd2>P>clb}EPHVOKpZwpP=Q5~l!EypLONu{WQ z|4D%a0rFd00{6GNxFo;T7R15B_@(Xu8lQkDpO6UmMWlGiDai5-9z2+DBz1HF2^mWM z8^rzPu$;mWNyzDv@_b@4!=;Cb^9>y##W#w`W%-2op7GhTWN{^n_rLqs+8-{|f5p`! z@f!g*{x6MMgx;t0EAT%%{LkO1vbcX?k7T$1rG~3%U^*aw`coau1mt(p;~Kc`3<)RAY&VT4o&^8o76EG{7Aj~0za@8kbV#VNb4uVD0ih$(zIl@S}^65$_9u!O$VC<)vx3<(skRU}63x-T)vPgaL=}Sl*q^0v4wYE@l z5=4H(TL`Lkn5;m4Tk z=QEK|J1CqkWY0;5$Yo$VT93(6Byvs~dOUK?zmek(=S2D_>71 z26V-2dF*6BUZBrprSaHt-Dqv8 ze^ggfOA3(q2y$+5h%u9uhig6t`@R|b1>*(u5@?D1D_dJQ%32W1<9Fyi&?RT4Lf7e_ zc)FPWetNv9QG!U~fF$8$p%lSX0mcDZ8b9r6Ys=&2#-{oPPJEI#2gKiL#$*-a8hNwO zC522SrcV-cp%YzBt&lPyX9LSDWU}&!{vvkXY&d1Y;M_COBWM1mL62&0oVqcf^z_Aa zdCHEQ<(mh+YGRM0=g%W2XZ6~d$fk~3Z-pjXJF$F{34W<=@hiy89NJ2|se%9hD8Bl--s|FGc5iM=5h zVzeM9XO?sCV<{(He~vvp>YX7L2^D?lXV}4);2ru@A2jCnLBC}FB-(mpQuXK=a_-CEG{JW42)J$|-*%w!h|<6G=}C z@;u0!am{DSmb;R2G+2;4#U;X1y1nd-eGg-;(v?!mj5)n!M$095Pa+Nhq7P;oOMYgZg{ms{r z^DW0hFO2AM#gd{1A40MXFC&|K2AAMWF{BC&6E71aQiRc8GIt5Ro+0 zpgSalQ_h_Jvz#YC7LW)22e}{QwUB$W^Fx*2Ql9v+kSnh5&o3o+;E@+V9s)V<+$V&S zSCYSKVn3A0is12Gygt4ga`iAK%YvlO=~tE<``HMR!T~z2ko&RYv->OQsz#*K8l>{4 z(3U0(BnjHWp*1za`?JS;jP68E6jNxa{G^xbHZ{We|3yrCJXgg49pS&$BHe{2DU=WL zNzVII-H3eRn%kZ+m$c)ZOOmtvLZI`X>Yfi1!Yb%Q^3*+djHlIb3KmH?0afUZ-O^t+ zlwZ(~7uYYzNaRnT`PA0v);)sjM%R^4BU36e$sto7V>%_GrY7jMJRjJ;g|cB*TVsth z$hmmxTbZnAk{>+@zk?F#1J`h}VX$A^pI&1irZ2%))6&Xd3r+Wn)7I3mZAP-8;P!6cZp{WKEjSEIL|M%t3R zIL=$!#bk-_v?Xs&UUVSWf;>J7^8xm_$8Ia6ZuLo9(5D={M8*J8%5%+b zlV*tfJf6wQAomi+uG_`L_G0!m$)6u|oD!KVDfazRYalDCOc`QC9t*j35|h=9Yi|C1 zIAu=Cp#bu+$xN0$k9;9V&Yk-@K-K^~Gw7+a>-*QZtDh*8AZ(i?xUzm>-6Qm0YN-6x zdy@o7-(6jA@~4mlqacYo>2sPWG1XKi%Yj`^lzl^D+97_xeh~}GKGo2vF#+y@v<70s zn85wYHA?vN3JI^sI6h(BUL+LNHZU4${b{}R0Qz>CaEluG=2V? zX|DPEG-~@$ z@<94>A@Y%&jd?n|ZmBjz<4#c70~bSV1*cJQ;&dR6))Dkoc73tqa2FqJ2lHDT($?gq zF!wmdSu6fpug7EM#%VX~KCBPDJm@tb4(f%n4PPA3|7+Sstv$V=XLlU?2b{i6Ee8{5 zR5xA+lT;^$Ur>RWWaz5pGg&f!qZ{^zZW(kHPyF&B{mV?OY5&yihVIyt*n{A;%US=@ z$RdQ32}R^VUK;D6Q|QmQ=H8e7ews6PjL|}{4s^<(6VJ9swFy1a))~a@F^D&$rn|HS zu>H&X>A@ZI>3|5tAA1_}W3~^C;MXOqYzA3n>ycG9gDkTRIO}X$9{n-b24~&u0UKqo zkynJd6uWMDZ7_%pePRR4^o6McDS|7S8Z95Q=h|$yAHh!&<6Y4oppiV#*3z3A8Da|> zoAgLLq@xei24Q}45%VAR{bumww|`fFL9(kyZ3`@*|JoJc+%d`v@h>30YBAPmxJKQu$6`{Czvkb3st_@u7Y)5wqG!m~ zGu$tTDG&CX^vF0(S_|=m5|qBwV9bA^PmOtoZ2ymSg)gOm&-;bRQjM)|29r9_ueyRg z3vwTNY@eh@4S&QjVSPh0sMqWFrTTw5T9bNsIF1+QQ-q0F*i*mSzlLMaS6zRfuNFe5 z89LR_=iWQDK4B!#$oS0dN2HvX(2XqZFK24J+XNl5zF;r=7-EXh6B&Z`ex1qc<l?Aa5w-jc$uw{;bhWzEi41iv0TkelLNEF*#^LcuTvwui z#_qSIc~Jk5_W`C~NH z>v0re?XO)jjh}SNaL!Z`Q{v{wx?%$5!jI}6H&dR1>Tj*&;{sw{mzZ}WAN5J!CelT} zM-C+8Z+!!zQyHcrkz7+T^+`-JRmI*yGHy|27~0ZD%!`QQ(v>J>-!IkWDHlccd1@=r znpg|1ty#qF`D`~+`j7YIOtK{=(8*s@Ye?eS*3>Q}@20gimZThz3(6nrLXRP~koq6P zv;onf#)>lJt-23uM0TBJ@ay+aBE9=3k;pcg-*rQ`l*MF;v&YU@_%GskNRKrlZku>* zy%gFyI%hwE>$FH4Qyq?RiNB4#Z)C_G=gc^BMZjOP6w8uD0r8|yiduVt45wJyR(A`gVzTCDB2 zeM~=2eBx)~A^!pS4|ZRr%7D8w#a)-HL$4Tm)siT0-m)TPL=_l~Y+Z6E>tWnWV!sFR zGzYb@_rN)8coWJeF8ggyMP8PSGi|@kwHNW+rxxTLkZZvv>XWMPg*B-v_e#_tdJXvx*hyyF z`Q^W{9dAwwi@j}zTcob2x>A(G`V2NTdD7-xPw7A&3As0so3ZZ^4|J%uK*nDu$g3eQ zWXJ!NCdR5ne%>l#&uz)sz}7?i%Y^smXucyIp5B!Q;;2yu0QAP!ZI7}my8$KD9)u^avauGWCp2Xi_Z9&~j5bw;Cz zXvVM+HmZ$9{tn>Z^=(oX-q5jAYGd!warRyoQT52cLY-aj+~*ul&`*PY9`woIETrQUnq855`}AT=-uJmY6?U{Jd<7=?(-;S8XE4kT1UWE?^pf_ZPG z^<1@$CBvTUmI~;RayBI?F@B9c{gDJl{*jUi$5sA^WT-Yg4uI+In`Q zB=d?M$em`OO_OqEu-mMmkU33g5kD=mwME!mcoEfpiLX$^^KkE({cVd{+brSK6?b1Q zA9}{nlh=T6=gu1rN~KHc?U#rjm>`}g#yB9HCbTehK#GWdvS<=LoGLrGg2&C={V~kK zIl4*oWYH9n)Bz+4F@lK!w!hG){yWMTuCXnL5#aBXxfV%V>_vVZW})73%`GFgm*=*{ z{_R|9zOABwa)5p*TVLnb5WBK}h()pTSwK%v6XnHQ_Fn9=C*|n}dEx9f?mqM{e?Zov zZ2!Zj1HEMEsm{STNZJba2goQ*&8rQ$qcklWF@6nnWTE%#6E&A_hK&&Ien0;!E%MNe zot8EG0%;)`Jo@JAg?=`!Q6|*h54ERHwtd+8#$0`UsAxgoTc?e+Q;O2>=F9=986MYp zkRC}*iv;-i|LQr=z7RDMcJeLSxX*a0HfBK9&ra;rtf7+!ow1e}OW3|3g5RLuM^Ups zJ(6mFVl=0}q-FC*2t$neP6Ww-$YP|;woBbJ^&ExT?;_8bXk>etr)1e>rHOdnOZxi- zRkqYZp15^S#Ndv+$U7N(JfJUb)5eNqw~Jrtkj}&|Te5WFv;g)zAq{anY!L_Xxowp5 zn4emU7DHYIxgb058SFCABSlY@3AOJ1RVLFr{(BybI6H(EiAp zhy9I}c>a)nrw0+yHHL1OH)rn8*#pB~5qVsVodsPnID6_#E`HDrgl;#{#dAp5jSygu z>!b(kAD|ii8hJ@m@nw&;O^pk%Yr|ejMF?=c3%Ap=cWeYKu!MS4hYcHILYEp+i3!ra zI*_J(0OmOV+`h;b++W(4A_{hFAZO0PS%Xu14Yq&ZzAT_02z?RKC%NrwBfIcj5#$HC zLomu-kdk-vtiPxS2GkZ1Z54UWL<_{7kS!oIF$;Fa*!SPf&)p&BeVDWs;~t5Ix`pWm zq;nw@{fSe?{M1lN##lWvAd^8>kBo=rWKgELH#K5`^0>MG_9cEFkGUh~0x~euqMI7Y zP|2e%=K9;mKVtKa<}Qs*lo4#JF-W*UW0O94M|Sz5m9ajI_^;b4hEM{_scoWLLbuL6B_^lD-=DdwTZ$&(c?f zoeeW2~PXR=)X%l>{3VJG3)u59 z>RFv0`iF}b4_vOA&I0X;M5i3s3>F8k3bgN#88GO+29QHViC zD^oJCInv4I+T!NNq?>cMpQ()yOv8CT|MMuep@w;*1i=vQZZO(5`wK#(-|5nSHEX>`WDL0g8?_nOBVpGEwQi$kK4eH> zkV%gj@&6_VXfD`cd<^eK@=bLN38z1Y4>H}cff{?GaNT1($EL3BsHaZmWc_A_G*u_U zcOS-nh!j5P;(0NH4A@jD8jwO7Rh)xD{D$cjDdPuv*Ua~RSGsA-#?!H_tg3F+jaj}gh#r9PNoM#fG5Ddw! z8dGIL)}d6H(9Gc^P{CxDhIeyz!LWyxg1>nf!rpmBnUF}_y;SOvA!#K(q>RXgHTQxz zAu41Q_reGlB(fOIiW_7=?b}jAG|3o)tFegEWY1^PCSe>aX=8cd9#P)xxm^N{8h*KL zTany9^c1i5?+1~caSDx$FZzvo6T-@)?a{VdMdkwt^*x~&?hbuLV;5)~h zccM3B3ie0J+gPgXv6&hRxO(hoMkds=5-sR?-0EM;Q+nL*&T#j!h&@;6F`>uAHMbo* za?;~|r;n_0qoJ2vf&DJFkB;R(I4E7(c)wJ{ewU8uSwpV?c1U01+TrT4zvIT<*A0VS^?xH@+0YZY`_K6*Q@{C4y-H};u}GJ%R$mr1f__J;@}fZnjwJr zl%K)ZyztWs_839Z5TA~*7JB6R&ooGwD&%F55BVFp7Q)ORzW}*4drm^_*%?YOMY!Vw zceRGyslR*~OcC*;5x?QVALHGAK~oeFwT345QUD$Khrj0SjQ%BgVE>Yw=rur3wWh!B zsr%*b+j94iNSfrwLDkUnWBa;TenV=4Vac6f5S_8mQGA5wU0faN!5z(*t3%4h7&^^$ z9A9Y1p39o@w0RH69qRj^gHS$b6{qgG_2>XQvC!**o-6TzOWD7E1zcWZ zPnS^qU$mVMT-{fF_u=NIO#`N=Y_iE7?$Ua;`Efp<^F9Cneb4utV87(boUyGuJ4*V2{kK>a{TRy0}`+#B`#7{oeWGp#n)* z>ne4cwKcKNuXa=PyUcHpzrc51qwOcWX_+5O#H)fg`0pw6I+M5TAGmt2{=ePv1|-kV zVU67;s{>i<&e_eD81Nw6C1f75++Z%msR`wYI1_MsevsIQ2smAqqqaB)r|5@n{B>#Y zOVml5uuGG2?}ES0{8aoN^6@*xH|a^|_ipRO_S^07JC-x| zsr@7OKb;Q5sbTyaQfcT09{_K+W2Dxq{!pCR5q~6fC@v0v zOmSHu4Yr>P`W9ytGC|L4H-6O7NB)-0SjKceBG^Q#oX;btI&#|ETOAJB$hiS{(jh^z>@X=2yE_6((<9+?TX}V#DdfU(fo}-iFV% zCGB+7?~gPLuitwGW>M4tl#JH4`w;y3li#uY20O<3v0Qn76nqMKZjZ@~v60=jGgAG& z0A70K>D5aw;*W5@X>1F8Kfmtgd{P+h>9zI=|1Ep$@ZXVdraC;B;$ZuNu4`G}6rWDA z7x81i+Rq^P6u7>LpNYR+FJK$&`c$=_NqCd+`Yq3tk7)y2bq)c`?NC4F`y$D_fWMU- z@?-FdtC=If>$LJsAIo0Ga6SWoYLqxta0dVHcdTuUvu%MRV~fOkstr!b&z-iMxE)I< z&jj8F-UvRbaCTbUkIxEY6nqB!FyHijCY?Lv-`R1N;(QoR(~0k-owI7by9gft`P1Hx zSoW%3j~!Q4L(_a)SUVlVPx0&N=U;Yu)y>njuZYs=?!??eoGLi8)8FxB>Bk|WXVb58 zYJ)R^oPNH2-x`!tL{28TBXpdZNdKzAL3Hf8&U5)Jz zO=rlt57!so_e7)#?s>S;Up&3qVY$Ja!N-B6uq;-k#nPgH$S_IL^ucJcG=r!$^9W?s z@Kf~%lh!if7iw`wH|?J=h6rOYHEuHEQbg(tWX@*4oIj0g7x1@)KUc4UI$+L( zn8L2t4%#jgA$HwdoYamwsGRL(r;Qy~bT_Xuhw~)pZ|<_e(uVNURztn}@^`H5n45d3 zAL_w<7FkF6_U*#dZyi*#Ihr>MG*6g4zj}IgDN5b_ewo5N>~y=DUEkvBI1|pq`eTb` z*VeN3y+)JTNX;c7URQZIvwoLSL2FxAAf>la)+Q9X30?1fmg055aYMAGOH$(2%bZqw z-)4?)B4zEatO=CqHxFIS`2XvtjXpH`O48)=#p^_4qoH`RTp;6UNqOeQ3m5RK(~?*2_9 zFFH{8p#pi45WAmUQpft{)rm3+>hTu!nh*FXfSDJ@;TCRY{3l+Nkr}sqT;nK^v!vUZB|Uo#!_(t57?vb<+bHNi z%$)8#a`s%47>kS?v)`%WAXQs_^` zX9hVNem6NjiM>}h=MwKCyh?Z@e3J(z-u?~=yQOS(( zr|tO~Cr^ti<%73{R=Az;2H_2aYw|B!xHHu<_%wL&wWn8$Rd?>RZS)>jHj=*pzM+~qpXBebY4T+vdjs3E zHc}oLaOFoe_#F5JzDZ*+MliEhjrIAVvg(98`ugOul6yby%eb5Rc)P8SD#H=nd;Y+! z4Z^aGeFzIgDb7Q7;N|!^ik}7ibn~5*=Vsj+)E#jz!QK3ZM13@IH}~e$t3%`l9#_v| zaLcyQN5QS(J6+vS{ceOa24_v!fqkR7AF3PPGCa4pX6v_t+#iXnGk?q6GmJ-O$xEs& zpcM-h(K`nHy8yygdS;M2Qcn(z@1aLJ8c~Oo{sQ&@S(VC8+TZ|Bh)r7fe&suZO<)Sm)tg#pCvnx z0q**cAP=Y?iT!fd*5hz%8cwgamnE+gP9K~J%L(L>ry5j!o~kYl!_C{8 zD9fZin#y6;&&^PdQbv{VViC&f2fvV9R<}M8ZB7icc2c0~@Vlx5<#9b4`(x+9;nQ*eOzeRc747X=M^}6 z;EanC?xW+0dYM?ys9ZK*N4;or{k0qG?AiTzf$z7)Yk@bL!b`WG*avUVb;A0KVZCHwL)yyq+XJLH&iwM6De5y65TXGx1Z8F8} zAa3I+Zd168rnnu&Z6w8Q5x2pRo6^0ETR(0wzJ0!i>re5G+40I8cuNkvCkH;91E0)+ z&pA9??^tkdQgTiD!Fx%6CcOA2^s3!9ne%F*gyxEkp8N%&I zmar$l4`;z=!KcA9rR^fP>Q^SbsKw&3P~5|Hqap|1?C`dbf8Tdi;?@nmO!$MgO|w@d zlR>W@kWO~Xfe3d#gzWu|u6P`OjkmkKlN5cJ%4r6?BLl87DSIV#8oZtF=&2&jmCT&L z?g!d=gLqBwrabSd^t>26bA|(64d2nd@KW|S#2G%W;k4vw3Hm_E#o zpUHjgIJ|LqlAEqh&Nw_>xtjFzYYhL5c#l4RwbF0qP1pbp=d&o(yh*>c}^~#`yN( zcv~`Gj>_*UcvWqg>&INBy9qhN;6~23UX)gjcvY>;5#aTPf_qsMehp6 zIECAT+oddVm?aL&J`Ow5$3b$YkyC{18oqs6ybeqF0(b?u>b%FTouSont$??JkA`qx z*TXi*Ww)``f)@+%e4nVih&y~NgnK#RHq!+!es_8~itnJqi$ijJUJ5@1UWNZW)_XoZ z;rYZ-@RE)oua*CcxD}?jE#sD#;(jBWWig(H@`7+{CmM$ zvfv}&eOd4+@I&AXdx<+ZyslgMKM#HZygGz?y{+&|;Dz_PwYpn{`19<-OWGK#!8=0! z!)>Apd=k7SgopR`TfyhD;Jx6>;ME!YkAN55ml>Y|-<$=X2XD%PUjpySf|u-~J!ZkH zzz=1?TfvWJ!F$0kWWh(k3p=x~ z=nKlf7)c+;|JXxrUw<$DQ`Rx!9Ce%$zN3$rFwET8&o^ptpS+7X8gd%#+`8LdS75Bd zWfzfT5LmOu@&=t9GA$uUNSwP|F;kkMN~njUuP`PRC$=^1q# zlb+AQae5e^o^s?2+)ch9$DGA=`(XF0ui9eQXkqRI+{w4NbwS+XUZ-=PG-_^Lr}!9@9h0NDxrFA)>V-=b2-n* zdan7x{vKniy3zbQkJrxEn%lr?7aGlVV9s0+18J=}Nfvxa$>axf9L= zVJ{5h8X%i@y}wPL@aSn{-xGa4KT)<`r#wL#+L=HMz$uOLhwOLEoj>M)>NZ7xySeSa z*vZREW|z4L5Az$YD?9eAJztpFW~Wbg!C~8Cj#kF`Lsv!XkJ~~(gXfs1D}1HKA}F}} zPQjbYjhJ5(&EhvQ?|pZoZ3O$r=2lAX^n8SV`h?3=z1HEri}2fw-@N`{9v$P)4(ArG zW7tZfk`3zN9ylBDmx)t~oI`L*;0*FjUYdTvpQ96g6np@@SNJwt-@U%J8m9~33*g2+ z*0mkBM$HbPY!}=f)cwKdZgW24+s~0F_}_dl^>*0VJi0lf+~c^9;;y{&_DpJXy6@@Y zAKHE@dTKR7oUM(bxy13_go$D;cfcNNd?F%0WB7?a&ia!&7X3=9pTLgX`-$O45BtOh zoK4jMIQsp^Zyq)ps0xJz)Gm8SKE%b8kxl?W$){k4)tH#tQ3#qUj?H~D$B>c)iP z2R?!<;^)(#T?Gun#V$RsYtRcOSC@Gkr>%IO2_Q?4W5|Mb69|0Znz zV~&NZ`w91UX)aB&d02KoW%#FVuh`%F=+afL^n+gjFNyF+lO2>@bZ9rVj0&Ji#dB&+I2GsJxlF^rqWnvuMmT9j!ojUL?CTuW`xi zCmxp+=VA6o73T&nEYbZaTZUh>cT?@Yf+q@Vbxm2V?nbE{UH{}2k?WK1d^47C++36K z(z3TDj&&{6rMuY7=o0(l4FweP22Q(SM_WNjTA(%COYxP~%p z1AHCibUfKP(mRByh>r17^dNQSK*F9W*#CBSajCzngtPR)Y8oj z_XTad325M#sRtioZ5^dv{>o2PFJ(WiyA*V9LOgLT#TI`#5@)3%$CGAZ{vrjn&m)98 z@Dc3r6s}uKc^fmuEA~3t9`SADJNi%msqlQip&fHG7gE1i*u|U>IU{_-F=-F(L@`zm z=EB|D0%l~+o<#EF_{;k!{jb_-ooQn>-E{H(K#tl;mw02?lv$o^dHsD#yAmBHl!N?? z{|fw%A=BiMiL>qs;vk&4m&;u3b*qeh&9TFdCnNWt+R1%d&mN1bDcFev_XIIfN^a(o z@BnufJ{gG@=(43#TlK3!H!xuPyIogbm`fsRpXBODu2MQ>EO^}&x^cnovR&dB8r^E$&Bv-d_N&DULrIGTT3Qf;w)qQbgJTqZvg*z1_+hTcO_e2d>q{roJt1GTqX zY(4O{dWvHu_{8TD>km6;-gJ0_wi0?dK$Ne1LDCH@jT^Y0hy3JUYSnW9yaK*D>cTdLU_IgVT@p!(8}1yiVf|{>mip zA@o%EZPvHXcPBf%XI>6(1)i!fch#6QZ&x4Be~M!xc;jEVdBcqs59?z(!4HCu%EN8R zd7wUJ4|vgF^4v;oW(@ZYUu4ayw7E9v_faCgvjmFqa}@58FD26A_qWWc0A+&GdlAlZ z!14XSFqcF7yfU^}CEcWJ5?yl?Pj1Z-O^e{89eO!AHRl+W5G;Y94g)X-dRP+>Te# zhQ4Og82$QxB>G!(hDs5%b<&(s9D>^kw}S`6Ehi4Ifc5YZTr0bm;4+ zS63*X=z#Ip@6I+!gJo|Gh7k4OeMr(q$HbX2*Jmjz{r&S>(YJEIeC7F@A@g zr8RF9P+QQ(+S*@=b0^>1+yyzi_-?Y)3c?(G1pV4GLEkLfyx_()60DV3ip}dR@r=&V zPL-zjns_$*VKp@EFm1ZLj=3k?X~K$jrRG&|Zb5x~`EM}(9t+ zj|YR}wwgP%!ta09&TFHUVZO+ZCd#2fYrPwEFEe4_ZwP-&$L*Pp=r1J4e>nf)gg0GL*q+6tjpy9ZSJ$LFv zj{%&Dz`TFc_-g9jw`oIKTYSyPSNluMPt-unovhm>twDDq=$65)Xf~2UObpoOzR-f( zeW*<0(Sn%CjS03-=_kyYMb>Ic(*q{Vu~2(77gih1O0zi+Ws)&sG$PkYo8qTMOo`V_ zi|ic{jXL2c{mC6hHxg2dCqwPlL|ng|Fx_2l95V-+9HZD|WJo z3D2`t4(aHUgNJutvGaER;s4r={L3SL{BGti{c*)k(`Qb;jlTH2J5%FqJ58P3q4TfJ zoGwZ9v2{{lNPkg7{8xzohJR+yLG@?o2dWb(x*c;s;I;#idrrs%)SnR0W%=s%tNE#6 z-=OSYn9E2;yOCk%KbrigecHZV+I0J+jbPW`kKw-`|2@ysu3n7)*Bbw0_8zt7;ex$| zH}AXZ3EqxKx@MK6N7nsu%3avx#SS~m9gAeTN>%>8X|4pdy1grQ)+>zK3}FyEJC`Z$ zWj?l&d7rv62i}qc@5zA==fEd(;Bz_fZ@NL03QT*b6g{byM?VFwPici zG~>kt+?L20GX@a1n&A2bENuPsfV(Rc?gHG-b!S#%eEW3xy#(PaIq=d)t^cYF{;R?F zAU_k{20jix6Y}rhxhTS|54<^gCduA<2)8(Hv5=o|oF}s2D!an%)_(`z(Z~MH%!d+c zHfKXGUM0Li!ZEx%((_{Q+ThK=YnHsL+f^$37vrZNPTqRQdD&Wy^18E+@feQg zDZVWHI!5>&@WLz3BSDAJ}kawLOIUyX}N`f*%3b+{*j+w)!fMJ>U!AMu+Iq_ZCz28w&heAF4AG z@a8rq*O?=@&ES?!w!&TlUj*+C<$c(0DEe;tyTU-G!YRRRbBfz$++rziHMkXr+!S65 zZbi7o`1WZH*WsQV_;3znv{C#H*iI8w=L4 z?o6|IBk-!o2gAEEJue3DFuZnnHGD_U{7WRAE$I@RKF|5aT8_dyO1wwl4D(H#LhGPF zy(ZqW()Xh5Je6Z7E;Ti4U!KBg^PAqAi%;t+* zCL7({68t6js{6jY!@8g7c!c4s((H3`ryrl z@d(ZlzJ1=>J?Sk;dD986@1@CeA)OIc9(p~S4vuq zFBXb-xbGRtg16#l!r^)cnEamQ9xs$K@3c$7*?en~BB#b<7(T3fAo4^ca=qTDj@)#N zDxA4a4UhU;8y+WnN`_> z5D#=iURh-h>YEGvBioZy9j1OBq5_Zp+8MLI9=&=gQIE}gH|9P-TaV}9UG%)~pUKEu zffs+3llOFbUW_Q7C%&Wb`uQfl{ygaLI*eY`cB*i0yqSInj^<#|_ZwNEJc!(y$b?q+ zRbOyD*V<>u3DEA!a_{=XdI}4kCLA{1D&KDkIy+!Q7ssB@Xr5Y{pftJf7sY3TAhO1m88GG&j!vkocDefPAkuR7#aOkA2l3Ok`HZn$i8dKOCXY3N;-dI+x%-uOEHzz!mKmSdZg!R8*W2$u_87i?v=hfhdJ3mvJ^Eh=0a ztlwsjzu+$I^cUdIUgvZmqOoL=u#Wt8FqYKdwv5{pZqn-$zSmpI7R+pPn)~u)SrKVV z>>}EI+{hwA?vS8cj4txq?y~(r6}gyE70Gpu zx{Rq~;Z(ii#bcs7B;#K5jzXjP-OC#b!s^Ul*BkzI=9}j`rNz99o0#M&MtslzG3}}L zjKBYo_siI(4W1gv6Q}U~)V(_T64(EwzR97otAJAyryrr+ljey~nzu;jYp&^KH`yqB zd5G`&@Wu%AwW?R?jM=*0x=~)X*u4|mr;HNT=7ux&?jW}>>d&vl34R)U0(|-j z=_%ZukzTJ_13m~I2k*0Oi#dh1Qxq{{`6!chR>#V&fIvv5;vD^!J3x zRLto0&i|=XLAaOUf1EfR!T$>W zOQ=K9j~f5s{?zm>W^d;{o|8^=pau6Q_GluHYtQD6#NXHZy_{A)w3Sp4dB%R#7Bu;f zTRkc6KO$d~7<+e}`pz95*kQJYOh4IuC+oWh?Hy^+R}fd%rutm{>gL#53eoEa%7Amqww)FUJq^GNV>&swFg&yo4HSPH}fyJvO$V|@TBrTGwC=nTi~0Ba!mxySfv4)n>)lK1657WShtzT!`rPrT=hy+iRy~;a*J(wEeETBiTt*8&bbz+JtljET3d7q~UVMsfFu=udB%C z`tPa`n%j`+m^(x!>uDmt!$-YJPq7i=AhtK$T zVG?!)Zw*Y~OuguaSMw3tIr)UGoVB-odr&XbPY%Or{^*(2PH~(r_%Roz@JaBIu`~7_ zM0bWH;tu}_p97!!7~>J&zHKs1RV_39@G^MC$In}d6uTgwloe10Lm4mk-zf!o%GrupPI@bu^mj&+x9|bQ8)hWLRA^&^82md^C zof`-5`b=hg2E6IBnep@Bn-68qpAGx74d51TK!uR8~!rw4L`5`|OUJiZ`yv>g1ra$z3g8J=7@WsyuZAETvxXq=wb*FI~ zz-=alH-g)AirXRFrc&Gv<8~;;Z4S4w6u0xZjfUKm<`vwAaEtNn%OtFeD?N}Mug-zD z<-q%L;G;S4=^Xe%4tymCUizUN>34WZDF4Fu?6iUJA%8RBeL3*a9Qd@ut3u&>vjLUw z0{A%LXTn#&RqtXU|4HTlWOlsT;R?&wpYT3nTMoR>;hFMhGzUJN17FC3YYeJ-z^1R6 z@93wW*1Rh1K4v?-i{z8xJ)WKyL&hF>C0}s+n7iSnxfACQoJ!Am!&;8=a(I;a51i%< zW5pzR|HK({M$G4X*j8W;eBv)y&xibnZ3ULWFMcsIUi@Lk`N_=nqA~~Gk^}F_fe$;p zgS_zR$4e9C&*Uyx-xO z`hYR;IQf$aKb!+!%z@{PS@|uY{12a7h=HqKv}V9-z>j1pzxEt>zr$4*d|eOsp<`Ka z^4$%pu@5Wb+`|RpX?Rbk=f#lM1h4oh*N1%;UYb6v8%~Aiynii6X=(i! zvfyOu!+OE{rZV?oBjAU=oVkyh0>AK;%=kR`=IPAs>QWB8SLMK49iFL=>ILs6 zewpwQ@JaBQd@|FmH^P0`6nNX$lKZfuxV5IZE#lUc;*`f5Vy3)_ZNfX7LHT)0~&`n9$4 zry0Blye#BDyw2zb?**5B&*z`tCzbyp@P6>5bx>&Rn*blqfGfCA_1a_jGz*3>j_kqF=Y^jeZuMv#0TSB2GV?GS7McTFwNV&QFqWa8}myhqpW5 zH1y&d_im^izOwd-D{A+xZ|BwIk#*cJQ1_vHE{@&sL+TsLn6Pc68iO_3J=seq8`ytw#^1t*` ztjUi!Jx^W8R^Gfbh^_ov13Q~`_)YNJ|C+Lkm^8XG#o9XBsx!s)I!PSY5#u_YF78Pj zDMmTX9SSE=p=LQztoZOVK^T?aM7PAZuRr1OVm1eUF$Z4s>CE!AHm-2E?%pQ+K$mEb zYDux#Ie(lQ?ev=dp6%c{j{4*Cc&*{54_T8l=v1ySvdq4bIhk&bn3^N5QrenL&ULN% zXaJLU-X!BNG7kO&GS(Rxe$PM(Un9zInnb?JzCrj>%?7D@t*$ycPMi5hYnwotVmcDl zxHqD^ug#oOZK)hi`9INaZP}W*nv=LDt~%}S6}e{bpd%>{63=={M}57bw2S8^^Lmim zkKE|onboBYCjPe0m?PuyCkpo!?B6)Td(T&lu8(|Phtc18;^Y?9r^x=3C(urvT1Xtx z$Wn76H|g{lz9VsS7|qn9ON86K;Pf47?x!pHb9Ag(@G9`5S@2fyzV34QCLJ@{x1jhU4%15WWC@J`27A-uPU4{}uRe?PdK8uKF5% z>aWz72KC)ot(e`t2dwo5?FQZ;ep z-E{f>R`7Sk`+LRZ`>T8h?eDuTyyp)mr!7l7yYN?e+WC9x^8Fpe-v#gQ@yqwOfWL!h zTzES#-=EHKv@OxDRCX^<@fVaG<#2DJ9ID|q;jj2Cd+zf0y{`xPqEn^D$TGYSYHmhj z`eWO^{14)P+WY_P3-^B%{~hOC_z%5s|Amhd|L-~fuYKYEoA5vF{r~uDFFgJD?>O(m z|LP0#uQogMIpY7%>~-<&_inu1V+^MW@L_P*FPOd>8!F71Y+o(T9Gp2gU3~lg91dNB za7zD${Vj3a{%z9zkDsS3!3RRP*FluwRt0_$d__3@LU4|j9WVFvU<{}o-jU}M z`vxYy4WYY_6qf-wi*Ty>CN6gTadSTPLyLX7QLrc|Y@tE1jHsel|I3kKM?rxN6Cc@vks)OnaAQ#I=#;|Onwr*)zhr|7GrE9`;0gyHYZQCkfy;ur2l^L(rPjNPW0zY zn#OFOk#tXsO;!uZ3D3#zNcnqr-dr6RgF~Yh;ShKk8;XC@psfd3c95d=o zIOWCoDg7hnD=%HLwyS>@aBd56YT%4{&Rf!RI^b+5c5-e^&uN-uZ4YNofwPWCc{bPB zcXY$4{J9_<8cPOnE5}XUy!v)?e*qWM^T4O?j;-xDyuKVfty_y8Ctu*{zOv|No>IN) zHgV~4b{5+Mdkc*@HSiDq{F2#=k3MerraqfHWP-OPZ%fQxwQ+0bu_jIN|AUN|1!+=W ztuq{zxRFzw^ENWw+RR*=;jQg_wTwKGX5dh}8AJBLUkGHE<8~3*Be+#7eW(61!rb4S zV>e6xh_g0?CvNEgbNF9eTCEZHOK=-Aa~t7Kzua-(pPkzSced1V@5#)qQX1ec{nC=X z|L$72x&y#o{^o8Ud{=Op#E#&z48IHbjlROAb%XJ1ZFk=hl%euz<}XQWEXXIdi3Qw> zaI2&Y2-BYR*W175S;eiogac*Eof}VVO|nFK@I?K5@ET_5@k*;gkP;-n5i$ z8&A+H82iE)!5@4NdHc&tX5JQ!{~zg*Odaz!7Ts`W;Hx1JT235J(U<9C;9RhE=CGUZ8EZJ@t(`oo*bxe?eh#3!%p+0Ds zur|MX$=RJhPM}9N!i@6 zX)&sdzn=S4M)R2yICEs|82`~(N7tUJUD#$Wx;=5ybj!|5YA36f5V>(<$QQ>YWJk?8 zfv}}BCeY?slk%nFVRYKRw`A61(Wn0+s8_o0Lk3K498j6J!95IjgzxB=Q@G|fEN=}> zUj?V{tJKM=rHnRFqu?dKZ^tp8r{R6>Y48oNW3FM#%;b4^pL;?0wM!ZIDpqpfrPJB{ zS3A5t6#m1Z_SOd8SWR8ExV3?OcXGeo4`=E1L0wW?7{+ZLw>iFjy3?HjoQAjO5A3+@ zdFgEEmf+RAAv>>>pbGylb1HbTP`Kf}oy|G$rYyM9)#dO*A^ASO{+$q$et3N~ORHlc zo=<=H+|o3>Wq7d=FC5>69QaBOy!30?HWIcrT|7d_(P0+Pxg5vk$x&Jltpa zbl&Oar{W!icNpG6C>)<}UN<7%QFy&|*?B7GWq1|!ne(mq>)G+j9C%9(ye9`f>~O`^ z=d154+fzN=u zy{k30tI=<;mf4ov9!hXKn&P$@x5FuJHMmWtxV7Lmk>b{g+rbpKKHNr9+=g%)4!J3P z2XWhjTP&3RaQixv17FI4M~`HeUzP)p=fFF1-~&1EgE{ac4$suSmcaYTzf5@auPt7o z{B<@B;ruNFAHshoJPtkvUK}c)aDUyA10QgBEaX2N{=pph5r=2$pO&)VWrSJ71YGfN z<~#bbPbcQM<~(2MKATQ>GoJUuPo?E44MXrQ!mE+Ir{Sfsq0$+S!tIXpg|!@|W%O^D zAB&UEAFki@Ed{1<#rQ)YYxeCE2$c^}z!8`R2_1_KNTuGN-R}COcl818;M9rug@P&)=9i{-fYCH)W3h zwD6Y9@n6V+ujIf>|JKT{p{{wmAAep|^{*QIJmEKnaBmkRybZkQ=JdGQT_1QAcwVUd z-s;v*{@n!4%??V=Ey4IC*;BX`;uhoEw~cV0Gw*PKi7C#9(Xau$Mx0W^mOLH_c**p z{?nvq9DE%4nbI=@z5qTY|1Nxg50P&dD%*?jdhQ6yR&6Qz_voi^V@OJj{}?}+Jrv<( z;EUixA)Jd>46eM4gEzP3UdML9y994ha$Fp(O>10tza+vNhBvV*b6!s7z~>xp@+ysO z(=zxx@-yMZ|3Lii%#2rpSAZ*Td^&s{%dI5~9s}=jxXI7(UcS4{Mw}5iql7WWx3_IF z4;7&pkjgEF^E8~2-Pt*daE9Sz3oH6G!rHUTDTmXSgVO}(5}ZAVBVUXjJvGkVaN6#& zeNa-m+&x4Uc%A()KX7zLIQr=itKSV|1-3iyv^haPf}iL|$d9}2SQ&kLil4xqsa7{6 zzm>D!VZSx_9lOW*eZvd$+lSwQ4(IpE7v^^gznyP%exDvqPG^odFXOlAUgvlJ3-ep? zQR?q~&hPFQ=C=#K#huRYt6rGjar|ELe!u_ZWv5qdYWyGR-|kOtb6$rK1D|ntHQ&@1 zGyd}407F|~^K${tr7SWE|HaJAD1BE~PVHJn=m-EbSu^JC1fBqu`s{>F!tU;S?HN!vi?4t{fE_Z|DzKXJvoc)7*cxO($| z9olu;ZZ1V?I`-^V{e?-5xg5#HVTCwsc$3}J_IYORqjGIg{bSq5m;=F?VD1vFH*wJZ zmuwVd6<{v33HF4{T}6@RG<&&{HIA(L?xocp$@(c^4B*o|~p zlifV^Wy{Da>|ss10a;%%vJN}j3GG#x`y-5Pl8I)sw)-MGPM)|a$>3nq;fyvCk(=2v zj2A>cD~nsUVYZkZ#D~rAE}oo{z+22;4JS_!pBO)TK291RwS8^$TH@r!9)I_MIEUaA z_c_kX({pCw^n1=vN7BmC8IB8{b1FTjbiQp`(&Pc(uGF4$|%zEq4fu@;Kd7F z;IUrwB(8`r&Xs~>xT*Ilu*PGa=8c1`(LMDajt zH*&AOfuI>yTp`ygNL@N#N}OZQ%HF`*`sCej$00wDEds9rKQa)^r#9o(g4+~smHGUk zHf!!EG$SE5bH6Rv;0KnB0r-dC6Ub1wBe+fC*1|V3jLy!^w`&XJXA1m6$dAI^@JZ%r zxYZDDbZK8C?~QiP-<(gWx8Iz2xb+#h8Z)clUwkk1V;%e{_})f9Th{)J!^k?B2yi|j zVQOx|ufl%4>5qdr$C0-Oc{R9|D*P@Z&)WDI`#fEQa-&T1TIY{z^`dQA-5w%4Df=yB zff#azkzM?LtB)a0b{~6hgk%JL`IN$k(+%IRxBNOp`6v7OQSd(DDQ`gvzXX0M3tlo$ zIS!`xAH#nY_yl+p-#(68_eQwNGl*jwoOw81;-tBEuMfQZPwcwU$1$9SQSdz(aOL-< zPcc4#S17;Na}%StPgj0lcX|0OIW?bVzV!k2sU+ubhLhzqU7nmWSI z_)ST#70U4AI)gJ|F?Oh%n!Q1YX@|o@v|HqtMTpBodxLf;(+~4FqUhF6c0On-<_@P7 z(mONkY`v&}1a9Zby1l7h*P-f0fWVXTzv9p7U!HJ&UYXlZC4M^a)46ZSzCE-0XVPh! zw#{Qj#u`kzi#vRoX^pZ89SiA-OK5=2Ixdn+QmKwuPMU@kWR{V8-#P-3_dJ~6W z9kS(#2sT3I$yn9T0l1@Z2l$S@Jm7BK8%?}Frn=U>K)Z!E$2Vy*{kpeviQzN^KKW7X zEr_ei(##7Z*C!^1ML18wsrXpX_sZW4Ze_R)P=~w@Kk~mR5ETqgrTkogGw_K-ziPtv zV_6)$@Y}Rs@a3zFjMQ+tAOn#fWnK_ab`DU67kKuLbv!wHLcKsfG>7T0XOjv$z zTw_%)VRgZAZ>k3QYV6tHkyK|@AI9O=e4cqXV^!XETlXU7!m$qfhcPVL<|?HwBCEIG zH;2CQsyexhtct&|Wu-h$ybrIvf$_Hi8Tv2APYrI}xE+#1 zFjfucchCkH-&Bt};N*RAX*F)k#@ct9%>`zeezp!pWi$l0WHNc36V@$EfER;HuFqGm zXHq^DehxX{y0*bXXT}NmGt0qtfqC|EH{u6gk1!r@$tZ zpp{A+_a^uw@JsmH$IY+Ns&VRa__W1?xq&g(-(uzlrk~W~L+U8aGj{s0d3~O=HJ5(J zoUM(1;{$5Pre1~5*Ul2gGI8io7|zDfkEvxiT?DWC@{&2zMH~}l!Ru!UTU({Qd>`!v zo^mRb`Xr-itQ&ZmsZ-e#Omw>eljg;iBiO?DoK)`l=fPf5Y(oJZCuH zXqU-WTCTX0aAVVsdv#{6>d)*VZ5*z?Q_DbYJa0d13vjcZoBI=`%deZX@=6$KtFokD z&D9sg5YzJo^5^Te9!5WH^>rG~GMpLJ*ZT~|*uHJwbn(iy zwwzSfpQgRW)wG?X-=$wULYW~qHSNle3*enu@WRvd`QWjT|8U#eoCf!8Px6~`;9WWJ z!5sLZ9Qe^3_=OyJ;hDAR52tT)4!kJ`-jxF%%z+=ufgjC*U&w(ME@e)C73I6x;WM^= zm~o?FFKPfY-Ki}w7rt)Ft20X)CP4PtRePU%Cw`Yyx9CsQm-sgdhx6}F3;|_8L+}>< zIqO+`f zQryaLYfN#g!mT>wX6ip~Rk$gBzWs&kPgf3nFb94p2YxgQUPhEIIJ`3yKbs#ES!n;E z_#FL6hIB|yrNa+~paT~nlEOPoB-kwHII6epAZT{Qz`KtKjaYjP;a+E&w znmv)c$Or7A*%vkxmF)%OT>AUu^3MAn?kR4?xGko*mE*RM;#Q5@Y>HbmZbwqwI&eFj z;?|4XWQyA$ZsRF#W4Ikmaht?#B*kq8x4{&*1>E+exLw4pFU2i-o;h5|O?9~hw=Ue$ z&1HmF<-l8W;JsOJmFe7H+VGqCj_!W1#%aHn9?lQhq^!UjCZ2|OZF*h|8D&o~*21fi zyqCdCW0TSdXWnyu{GK%&#jE0dnD11Y5p{ftiq?4R>$E*zkn>}kHOu- z+(+CQsKHb>_aJv*HmI8lYaSWBxS6y)WMrhxgS;vBZ#-{DdR`29c4!k7?-jf3# z&Vf(nz~^${%Q^7k3z^fedRgi4QNH~+=lg%Zo>o0+gV&Q%PyBc&IeiXayn#b)<^poQC!#fYJpYPOo+4wb> zc#XrWIi9@k_iJm(nE`JEpAE_J_Br9R1Q+2g!&?aP>@*=>;Caf2lJB!uHJ`q0gjZ$3 zP5K?)8w$tgcazIc>0)$-BYMJ(6Hh*tSZABv5{3zF_q%m-gncygX>>UF>pAKC-FNx^ zHh+!&{gm_f+ROLXfxotIJAbS1zU+98;_v8l&fm8#-`^bmsu!KV{g>~r@axR0zU%zm zbNT+__$xc({9SYT{`&AYyyX1-^j(*oFFM0fc+UCz=H>glfWN-)Ie&XE-(T5b`oHte z-|oxz*M`52e{ud^efj=|@i+Ip^Y>$R@LgY$t6m?)U;JO4zi(WgKefl1AJCTmEwjzi zdGMtScntsXzoI<8Z~LL>PQrD2*ZvJxaXR62dd@ZJIfHPHc+S=7ITLVV7hPE2e{@Y) zn+a85)(ed~e9{U^{xCN@fww}B2sv=LOx zY3mTBz#OCYJ_tv1;bFqMNLU;A)_*GBaom=0>&)j5r;F7qEPmXY0pGm5wAv2r=l1?= zwfvt4?*i`=e!X2wu#DKGKb7C{Z%~K8NBNFE_|CNX&6_`Fb5r~t_?7=*^$*cEr02_w zL;=M4(HV||@DKAHeW~Fa{cpX0AHzIDp{+ch89d05^cVdhekcBe{P>YQSIk{Gfliv6 zoHVYuvEoL(dw=ZP&#_Bo1JG_y1^zGMe+2(SeEYk0u+vpgMq{(x{EGpmGuYKwR`w%) z{Wzfqw|g0QP~O|QgM9;|30_qWp2<6S-SA9V1bYS=9&H@#M_mEIs-7N%e;EFJsO))- z&qlyM+yGB?XBKYHPlCE5|4X=a;?`^Zn>h_zO34Xu{)>Lh`0<~Cf5o>9w{hGqC%($B z7Wm8XYa;xy+txMRkf;38ZomGm0qTQS_+N=L*Fjk}JQ2O;m`t}ce+!pjs<-Ld=7GrF1RSzq(0o1gr@8N_@o!j1{3sPbH&xA)i&ZtoJ^fj#}%$K?LhtTI^fY$iq{-^M{<8;B!8Q( zMB0sISMc&pvu`oNRjuzdCSGqdzw7j_jb6RkB9Dkvome7_{`|9v{zUEI5^g=Xo#&fy zgL$*r#3kSB3e0}Fok7#qp>=+f`WQKD-kpu!@iygkupSTWG9MzDwutak#m^Z2mhflp zBiU^H+3?)$7ivSU-F63`*00WR99rl6{^YGezjPUXOJ+!8)cO6J7v{GGzt!uV-@TXb zS9v>x-=Ztdnz?wiJ@CuMk=Z#@9dO17W^Yz)>ttM!GWj zs@I#P<_y8I!npFRu@8=3W%v`=8S$v!?!*E~^-yOx_ITde9_3$JUL0QDhO?_(YDa%% zc)n~Sx^hwdcqu;>Kczpv>a2ZJz}d9;{kItSAaZ8&+#cN@Atz;zPMnD_=kD~JxiF`0 zEk|*gCN6t^@jFRxvV`yAT>u~VxH~@)KHsqdK0%rp8WU~wO|Ff4Im*Mb71|GJo)yo@ z@oU2He%GGAV~yt1{%4P@NqZdc-N=e=JiAKqCEA^Nool;__V&4FP$XF!kqY~k?Y?|f z0-mmQ0N$$knL^GWavDQp0=5{#>#=5#xt42B8Xepxm;ahCTaK6TKU{FuzUg;I@;<8C zddYv$Hy55=o#dN3WBMkG^szxY*N9pcWXPI%){@~q)4s=I!#QrnY>y-ICdnrpJouieO2d(;_@nHQg3UF19Z zY9lA;?@Z?3qiizvXljru^OFCj?|8{sW6$CHIjiFd#{3w(8hEqtWUCI(ruV+&^mf2G z@>2R=S5|)SC%osZFbClsEIw<_8~Hh$_LP*@zKyA`n1pu`Ub~gYn4xY$)di!Jeff`s zE|tk^g1-bm_VZ``o3cULw>}ZM@fcPacARw;PQvPs$&k&Az9HAzV>WLpS1DuU4Io=- zP6Tf(vFwOZ4%7zkBD@aE^Zkjk(adwG(WYM!cMsh1mz_1bUZ3C0!Zoz6;ZDMBzD({S z+#a~O;!sK)3YkPq!98T-YwG+iWk3I^9(Dc$^|Hjax#*7`Qhf~Rv`yakg!5kQXcRvk zzi`&tL>@DKLgn>9LKjrOQ7=(9J)voU;xa;9ntsvsvrbp<`sKDvIRiEyJVBNYrh{ZS|w39*R>5XEb)! z*h?bIw)v2Ke;J%sIEQ{Yx&QR~FW--vxxy6r_bbSv9HS@Sq`cuhWIOEd!;GCq9l0x1 zsZ9GvcJ;5?It4!{vpN}m_7YWJUbek1!7u-{v;MBqpuN6Dsj}@=aV}h!pSJ?9jc=bv z_MVX-kHo8lSMe&>pY61HsmtL~^Rf+IQTbW(ZlRYKX?H~A6!X(BSwDHs&L>>mQ5lWv zj;7x@yIP@qIn||n(Vc0s|4Y zmIy9YKk%V^b4QrFWHCW2u(vI0g!JWVPJQSM$n{1f@lc*^z_7dYHD}EoR?(|XTx{J? zwXojJP%^xJ?-g~jw(4GUJ*rs;H6!N|a^gz=w;nX@RWUQmpE{*UGj^@hB`z)_SZ*u3 z{ls>}(WiSLub()LBJ1LBGp}8btjCQkjp1He;y=-Ths+UxMt;|`(8hkh zVEc)a4enyJdQs|6oZPN|-+ba^-9~-HSz+qmZ3T7u$Bch5O-8CnkLuFznK*RYb(S*5 z+#PmXsEO4oYa;JaF(~V~<9Q^1%kg>RY;+}+SI|ei?yS9cJiG=nBb#YYZt-f@Fg?@E z6RxE%pmUENf56sd8v!HRFX%dw@_;^*XfF`=Bd=%fsj_$qzxAQ<*4Ro)%kS$&q|WF) zo|W}u()`ix{}b=VHw5dzZsb=prpIwRFZmCexCQpj=8C%pQ-2Rc-pw7KCpZ@3!b+{Y zPPyf+pl!FJntQ@p^SORT{nrFxPSu>X_jX=Mm|JWg;`_(AOg0;=+a&x&_(ip}u_$r; z-u=oyt&O}buz4uJ^a0$F#yw`{tk2uE5Y_>b8_f%_*Xi7kGqu!J`D<HEK6k8^1%OndQFe)6M)P~R1omn8CEaZ%MZ1NeiX z21JXAAFt&hh>f%$7O(uiTfep*uiqc3+jzXL;3mOuK7QT)i1KORs{FkE+uXd@jm_!o zat^^e3@?TsKc4t~P|cZ+rorit%=}H^TyXdy>%YR!!-wywW=p^W0F`w% z(;v>iUWco{^0-$?%g+e-6!Fi5Pk~>IK?Pht-2b zAM4QA;@7Fe`CLk%VXGWo2fWR1WG>5h^bW%d=8+hMb9CQmS0qzsI7Z>Ni2K?knRwgF*#mBOp@ z^Nzra;m7mBb$W9aT;Vi1yglSUUEAq_w~`?o$r*Ndk=2POm0g!BJLTUryz`xmk)bvw zy{gJq_yYKb2Qs&j74XIfGvlQg|L^I_j8}utyeV^jwdKJ3a^Rym@M(uDt-id%^=HB1 zncC0__%iWNhsO}v`WoaUQB-pr-pAI2YWZomc+VRyiRzd zIe1FP5WGovvmtqYt4newz&jpF#(iD(_ABDe!z=7Qo72uE?Yfu*ZD~DJmG%V7V6x; zo8euA7wbtbFCF0XXD-!tygo#7+rEqL3;r?>VP@Eq!+SW|2P0|6yBJ4I}9(W ze}tOoA1B}zz18X&qOVMnNw;PDM}!SRp}Efyc!h6EUxqQf#!jQ# z0Pp8J`qZ6i?G55^I^m3pvmZ{N*EBm@$``-?9fQ*Yf6mK%aIH+0Z5tO8SG*m4A>ZDf z!t3y2I87m=6}+GC=&RSt5N83-q~{c*=j5FsKYOh{Hv0UoMEb&K_{-q*!P%p*z7=r7 zZzVLsS%lLs&SwLTw*gW8>4H=J4p+|ahm$6s{Jy{RFQf1e!5^2*-2oq++OW|{*mWUl zR()9A=kU)_N8jo6%iHaG*T+HmIte}mKFD|8bruhgV{_mu;O)ZQ9@S1)Uo@6iERlvs zogVk;ws0DT*R|0YY@EzmKlbzVH+^UA9o0>CZ&_+?)d#vbD+&s@HkwZ;V606S-W?=!^9(3yctRr1NzFp};?=0PFVy}dQ>C$J| zjHxudLx3%}NC2-G)`U8=>PShSiO$<&rS{pACR&4m}>QW(Lz0ZVo zF|7ZQ=H(W>MbkEayeU+k=7?+LEmn~ytx%mVkcs@4^oc057EW>by)XN>pE$`SBMH(L z{mElEW(P$YAnl^ORgU-iziu!T|JUtFf0_3tbhF3~X)yKis9tX%gv66MT(SRTFd@n0k(#uB=DQ^%<9!}*~%U5~=4f}^pT@-Tggv9F?vF?N~q zZ30?`Hvy0CC#5bLjnWR8j@mxH!oo!DtpA^B79V45A&Sx0wx+Z*v%$*K zlR;i<9GS$=$j6@t@WPZWiicXT$heV^g;#oM1~k^@|Y6HvRi zVD~i^k&*ZLv-Z88hL8;PnnoR(ydQz6x20@FnEjFR3+!unIgy`TPB(Hce!<1(Yd0tA zqG>&s86V}%6mrf_FmF{Eywk|hS^cEF5pxZXdG&c4dHx%2Ymq{b&kd@jBMz2sAC`q6}5Mx5q?f7x;^q95Yqi$YodM!Iwj}f;%{4`Cwe5k}v%G_fQ zoH01faAJkXeVL1k-M`V$ZSH;B>W9A@%r-Q8PrgnBV3RI{I{N&(-m}t{}48 z{IRYu-~1bS#Qsyq@U7vt-H?b!5kD7ynfcu}gZ85GtGUP=1GhrW6Hnc!{B8Hk?C{=z zxLt4${~h*C;vP5L?qF~D&V(+xkvtrM+xZXJm%SA3w+wg8-Ki3!ThoA!g;P5dDc&@e zxRg$l4sCOp$FA;HTFm==TQ_P(!Q8-Xg^Oe|>4Evsk?p!|`uB$8^IP@r&g1ju?LXXa(BF|q=a09WiH>}k*^-&Y zFo!dna9d0O?3J3C*Y$4tq~`|f3!3JSH%dnH{PCS9PizZC-jL(QD`u`cMUtSpf<~gD z%k@-ladsGkBAiGRp;F_Ek!&FNH}dq!6OB~r+;Zy39LY7lZe-_MA8jX2ZqvUvX`&fSGn-GIxD}zE&ZL#Z@n0D~ zE2O*e`^h>ouRr$p@rqaWt5oKT$vmrvNj@{y1aAsn3_p~G(c6Y~Xe)Q$QM|0rQIq-!X_=>|7S06t=?#Qk5RW|}!VYVcmt zcf|TPYZ7kpct=AicHwQ0f^aAucN0Ve&Kvf+(+;9X5wvz*9-4ZC>*bk4c~jw0&gWldL^gF z;j2&y`@WwCY?styBYyU~+{9Vd|oAm#N^&d{ZPggh}H^Upt5RT+DIlMiTuC)7K z@a8kfQ5g<9yhd_T?t+iuGzoqQ{|mxd=Np@nHzw=g=HX2IXJVdf){)rpZ;}_yV=8|_ ze+WLwcl4**HFj2*c?{|^ZlrNL3wG=+do}hMKMndt)uSrhdT>+KLdR-#4O(9`XeDQC zrEU)2iO5kczf9MvO{?uz7J7_icCDv$48VZQmcc@i_L9n)94haC+bjDDGDWobbLu z8=N^fd&K#EV?vJKH>knAA5P6G^@F+)eHxC-H?tHD><*0z8w()Jg4V9_k02wOch2;o z(GP}XWM1QUr7RQ8xro9Q;)|R)#brlGj>^xdc6RTN>^|O^P76~+T8e&)dL22JI4h-k zS%zCDZfZBA&5U#YEQm_rz ziY_2=wfmsku?uj>oMXI*wdm6iF_hZf~jpBhI?cK<)wZo*ozuor%N@J zxY~l?-D)VBw=Cmle;Wd6b26zAH4b$8|4kGElP$`We? zlRHz6I>T}Fm(E!|!Sh>`f4-m5REIsK+5**?Le4u}g4Y=(j?WmLbV}Y<(k%0K$a1ks z3(t0J6Zi2M7JbuVWk2Y zwR>PWsYsag?RgDj{V$*M_oFp?-Rhe{W0C6f1=2V6E4HmguOxkej)LnU?A~z`yka&E zmf;PshZH@-&6}+0O)h996s!!*(kZ@a%$^^r4+3V`19|1Y?%La@jl5u=C)C~s z;kChQCl1la4bPX;@H*GtR0k*F4Z|zZ+I6?#IURJuq!M*vX z&gkz0fxr3!)!nju&Wl__|4N05uGA|(#zOU?L51cQiMv%Qk#{YeinN9`j`e!XI)mZt zTTU%pm!h8JwPMOveQL*R(T7(i_o=Gd znWO($7t|v)bh8Rq^HVn2;AmO#;j8v;jMP3E={#k_qW^bzl(A3IP3TuY2rhdxb{Qudo#t5tZS{K%V+W)aUY$Xp1qxpH2uV?&Jy?9M1ELHGeE@HGc zrUNt8)F=>5HB*| zGRSA<>No1oS{>}-*Y;y(+nKK>Ly&g5wty3#rj6|RUDiysK^ut?-aO$g&p18w`v@;Z zx2pbN1G)8+Y~nkh&~hAe8W#aKb7klV;jz`mR*IvoyK3c-FndF z|BY%zb!t1U2O{e>=BpRiPOaY%>eSzQfx$bNP2U5N9VhBF0I&gkfKB-m8Vw2}pTs=g zv_rOacR9zrQQ|N~9C~*$4jcX58&dlu-a6lRrO`j`R9Dhwh8weEie56%wPo&??+pFP&Bf4iSOcB}r)%ae?z-=|L9Nu3JK#h6l=L+dOC=?0I{N&15+ z5`K%?fOTfGzZn@r$e7yAxR5MkUw(ty(k+Q&&e9(-mam+OdXR42Xn&ZKxC(>*0zpgH-r3^i_AmsI=8y0 zdi*IPf5MHes&(cq8r5ZUM&%~yiz5%A1wXZL;$))WDAlz?gVp}uVpQYpES2Sz@H8e) zy)GYnWadFOpX{EhrVeJ&6$y5knNw>#JrBR{-o$uobZLIwA$-HN*l}jT%fSc1-FUpl zem% zHu=WfX#4DE=4wR`HYDnwnNtJb5$0}H!^gk&POM&Ig*Z3fAN093#ItE5>loZx)ef#U z@eK8g_CQ~vHAmi`GUu^!TdFxf@}@wp#`c5AjpNp?e)^efmHr`@ekI-1k?lTNk=-ik zx6dDcC}lIu?83y4&uV)Z#Dw1; z3V*xu&+L|&PBE@dvEAxtOcrlH#+foT-u{f$^m_7^od#PX%6LDW)Oz9GXXdN{j2Tvg z?FqAeR7Cvtyq^B^ks!`JxXlyiaoko4iSsvJr#!pp_F$A#W?(iC6DXv|Qi`HaNq95e ze_g@4Tk}sX*h8XYnBWMwec@EI;uSaJL!k?LUUa_kN4 ze|$J77tOu939B2om|45{_^Bx;7fXXGWg~&LXj@C`gG6^%V5$JvuH*BkOb_s6WILa> zX1F!sC}Ee6F(wfvWpDc&229PgnWrUmCP(1aya{-*ki767SG&U{Gd0iU)(<|3|Mn2>!wHwoIJ^rP z!jYUAhmWR_6SfV$2(SGU>EkOoMRgV*OC!h3{cu-;BH82QvVItw=U*`U8J#%&3=8M%r7HcJ}R0+H;Oj#``{OU&hhtU=c^y@-h%!LeoS#mdk@G+{yUS5g>Anzj=#3gpR+dX>@Ngo zcRLber^eY?cuVl+`L^=?8ivar?hnkQdVp8p4t;?>PI0Ms`aun0^_sqzS8|NKMx3_* zr4059HG4K%JH*K@%AjPIJHyMlD|Ql$^1Y{lv!fG1zE|M3$GKhNTk$p~@7HlL;WIn! z#H-m}NVx5phP_PT%$#BxKYRWn(Pso}8m6hH`q(yM)@Efo69JbaDS6Q!{I<$9ogHSK zv%~C4-J@wF%uZy?e~~hXBI8R&hF|AsRuQaol!tb{_3@-b%I=T?iDe~=E-$N9Tl9-<5sjOxMyyd}cu;G6RGHq-8|$KWi6xEmU= z4f*nOzW?>QT^$vrt_NI|RVCa(xQ8UywOgaxVX>?0^NfvdgK4#<2Z}*E$Qg|2SDYRH zM_;3U(8Sx{lOwq^aH_t1Zgo&|^vB?s_TlTVwWHn=8aQh3vx1+Yubx|7ke?3g$5@a` zjbpSj_SOz5A0qD#RZv$JYQr_0wv}{8~&1T%#%A-ti!--p7{&@icNlOZ} zP203hTiT{=+NLel*k+HJ!-+GFP@s(#HCnW&QK=6q)2QqjTbW}|c?l%8abqJufS~Ld zn{5B*`@7D4?)(1#^Fp_M9zLJ&zwdKh=Q`&;uh-kT&N!J$blgz91u6->QQ>OSCm~nb z4RSYd+pC7{{l|_r*b-oBBEpN9P>FuX&^Z=BxoQ5|<{~Hl)jws9i~p~+vi)4p;$gZg z^NABNNg5M;gyz&9=p6qsYr&P!`3?6CA>S@K#SgLYnLt(2s2_<*I&sGccZs*@Q!y9pq2{eN0nrd*F@C1JV{smT7sH#>a3dtBUAS z3CzjyQSf4R>jU_kD^6rOw5~qy=*4<73oyh=R};QGdB}4^kXiz^_V- zu!+t))#%P@0Ip1Y*%? zS7(iZs`2W_DQEcV6J92=bvNnXm0C#!e(A)du^38#m;UnP@HFs_Uzr@<47`8tiAiOo z&db1iF9Y8@1-yhdI3B{o{mQ=I`!^+Jxmk70#Wzk|PtjQr!fo9TW74@;X61$c{GRX{ zz+3nx#wE{;7b|ZfztVQ_Mso06y|VpEd%@fO=85Yyk0SO?0k`iD;p-ChasFn+q)F%z zX4NxJC}%1>mA44I;&~?~)gv?jp9j1?QJ&WwDSsRAdf?3oxcAKxyc>8o@VN48yno2?WllzQaT~e? ze`}aO^jiv57^eHy8IBI%J!DTk_gmT@yp27QQT$o$G&scw8>8Tx-R=>?k9lRwVKS)M zyX-b!PO`Axm~UFhLc24Z&}sVB6D}^=yAtJ&k3}5Fj;Op^bGMVP<=0MR_G_K~#cy&y zSNKskXz+y-$p=#fzn8xq2y3%SPGmY~kmptMU~4Rh{Wxxt64O+*rnFrqkBZDmxqF!gfjnuD?`i*^n`Iw@9AGi8#Pw;n@Vl3=zMOtIA(#AG1&>7fSVcaG+ z{04W2ZbyZyEt|2S;^$=FgVDFaEZX>YP@mEg*7NccnSRaBnO2|Jo)+tmRw+5Sfqn~rsdr#2 zNuS6JYaZ79y83(qzBc#^V~)HuX582V&j!~Gt~_=$C{+td;&I1Qo|W4|;?Z+%?C+G< zMtO6Woyc5Lo1J}!%8UJPf=c~Qx$FWbWU(Wuh@wM9OmT2<|wPc-|QB7E(c7KZD0u{&LyA?n9 z;-SN7!KhQS{x~ajt1`*yXV zuqhOo0k( z`JY3N0Nzu;BU$PNs4Tou|4*yCPjBUg_v@#+u#W#BSM3s)yPu`*XPJX7<%b$P5~A?d zICEMZ2l1xWHS%)Lw7MJkDW6uSJ%e?Vwr=Na#fEJsG8d(T8MJme5U%A~#;ZK@VZ--X zBF+waA==sb=Ru?MCfV;M=XZNUxb%kfrO6ZCw^;f20&gOJX9DhHK?!~w_+H>$33!s< zOf^#eXZeN%+{aI8CanZ`@#jutb}|kqKh~Qrm9`X-xmFJC7HMHifG0}1KhzNJ4%!R0 z1lkR5G_`73;Y!Tm7CUoGP<8~T1g8_-NIC1Cf5q3+%8BmBp%}?VVf1Y?Z-mb#UNu>gR?to zSNB{-snY4le$WSQ!&Z2%tPB`jB7W~cvQZp zj(0F`P_g1{K8cjgGn58BXl{q*@voe)yHlultY0%niVATuSlw~ekcQ+al->TC<7evV zErH$`^tOKWM3$ew>8|5C;y75BxMydnudmr07T$`z=W8ci?9+6j9ZlWDsZrM)c^#D1 z2yPd+#~Dl1y(3%~B#W48)Wy4T>p;02Q0gPuBe>1{NN&+&f-iR~=j6U8p}&uL0mWW@ z3n+G&I*fgtxyv)LqHOJOcPhARx;3tnvUO1t_Ni}#I)E^?x!2LRK%(WH>|?;qp85W_ z#e9!;mZjo&s?53Ab!Vif(&qETmeiUo*3qDXgr-VZC@*&?)#9UXr#{C&|4K%3q?Z}y z@VJ}wOQdT~_;k&+1EkN{73yAYbKgr)Xp2v!IXedYAn^Ulmz)RkOmtJ*`g!hc@K7&C zx{!FjFgnb+EW|B^Od8yNa9iY@hOCk3XS<_A8-dn;X)=Q!k5gq@oa+tH82N6v286aa zGbr5_)vHW{{T^}lg)4lRYi5MsVcZy3pC5$Q!arv}2-mu3=f5YWwRzg@GY?fb-?-H6 zta;xH^$}s42-UASCBQqrkG>Y#zRmpp;9?!7;glV||5aZ!l5cVU31@dLcl(B}p%UF1 z;2TkZ%>My$2a2B5MWkV5{Dkh`eDbsPz(LFS)^mHu0JM6Jvc7BGe&1U}3t5*f$AXf$ zaM9X_KAa^~GhV329Q5r?gVY0IBK6Cbw=1lN=IV0w-ky+x`9+SC@;}`#*PKr{L=$U^n8Rd0D zW9_@~4;(#_nKgrUd4T>YPsmi}>r^qj!%mLePuTINr{kT6|5ES?PV@x#9V$P};ACct z|8uKB#}$68CTE1k?E~%<5D}MXY-lW(;suJBe*(l|QdT((&VMk6sn?#LpUBMlb?PWyPaeMifV zMgG!jEe#-tlM3_Wmt`{t%ZG@)@lWVQj+Ht1dx86x;16TmsyMmeO$90z@)*|2kTHAm z*9)=SC3ouB<8P`Yxc7Wv!79# zE2-?6E4EEbOUF_1^y@ayOv{pH%T(kI=`o?HdEW`$18*70^hs8KkI~(p|LHlOxw?GQ z8@gX#vAO8!tID^`-g;%lwpq^m(lnLRYva`f0LB?q@3J!baO z+d>OzyMtGSuXlvM!5-3ekzo;aZMhpCj(3k_ovrL3t)4WRBroq4r$e}#kJTEvo1~%l zN{2U+IWV8^yTiV3IMiiH8C4@ileAcM-G!TGm8tz%Fb*BA3)e}v9F;}D#}Ql#z7!zU zXz|fj@@V3@%1Jf_-B*YPI8nYb@?ATvZWTZ6%1{`*h+xFqs%dp{e1(#i>PV#C`rgKM z@bfpt-@AddoxejHkTwEmo*K08N$6?RwB@t=uecGe?&;|q0L0~+j$@Jpjfct(BP$GO zsx7x^?U$AQNtcy^)<6ph)6Lc=cc9QsI_*!FsNcSOXnUR+H^v)zUP$v~-aQW9o`5_1 zOw2P0FAZMty(5>k_q2dFkb^goy{8+zuKPx^>>Ej+!d+9qRiA?)yvx;lp`YK~`Ps4F zFM)TFa#|AKC+852y<~jMl6#R4TP}}R3tlI9Y(aT>%N=j*<6%^UJeb7zE4&5 z7&ogdq@6p9zY(~T>g;TR^*F6DXkS0>9tsAiX^7`)>9Y2UVJTzNS_Cs}R0s%<-< z+q>lzi|3m9cotozk;9X0-2$zC@OpTrzT@;8+Q28$Z-@stP8mz6$3FTwb&ZwbcmVDE z`d-tM7pA4NyV1lj`4Bv8OY<@;4_=cl|1-9dyo=Oj*P|nutxE7Z9m)9xcWbY8J3AoN zyJ-pQ+aJ5!7^zvAC# zPJf(l6dyPJm(+g=e{5Wa{aKCr@TsEkq0Qhfpm`d}yY~|#c86K&;GEpNtx;Zb2!A8^ zi@HWKYc)>4X87_+b1~by^V=hQaEtltXk~tVDw<#3?@sOsw$?X-(+kdaa4xyNw{aic zNo*F(;1;#?-N%kfZ1wEE(-E3glhrWREhIb4=E59w^QGM*E|$cbtQk!$a!Ds^9NcRt~zZ?r?Z@Q%yez-&?o zUV*DP?kf!CZy^8hXGbzvaP{5B-^pgBJ!z?C=i}tJSEs$uP_BFV;BtX%|0n-}&xi6! zrQ?Zx{UZKL{DePeN-+cT(NTP1B(rb^ZTRsws9*ZR`|>0QjMKwNq{y*M6l%{ZdW8P{ z%1EXb*@Rf(Hr8(0G-KV%f%lkMqmn~wpRHu!b5OTw#)_946ZNXyG=qCA*~80NW4^(j zs94e0M0NB1yHxg=bqRCKS}o$i4FgA7^*~<+4M*|D2y6^&3$CY+WoKS-w9HJ(b~hV{ zWt>08U15dBw+j*}G&kC-EsLHp_9Nd89HCxX|B8N#`xU;;`b%G>UVgpWEtc^lT$rI; zTwE+#rX^;SOU!GB?;jexR>mI*A-5ko%&B4%LIf?>D`jY1=q*Q=@ZFJ2e~NORvvPEw zp^M-HZsA_iC>vI>wU=s(~IA*m9(IBXCF)X~5MPn|w z%ZU)}gG0AT)UN^0?CZfFBg3yz(v2iNp@)LuU~uSGdpezRQurNwlM=YP3oOWeV&-6a zsD|L1yp|(CL1bxR!H>x3WQ*V)0C!rMHxCX`vz~n7L)b6N^bqQYp8WbyVRQZ9(Bpc) zGo*b#r#AJj^`qxebb_gqS^ij_XG`+5d%>H3#mNbITFv+4AzaV&?>IRp^%!G-{EOWl zXmF|d)pkz5S7G&1HW_`_`JubE`Lb~pQv4x))>OYzxnzn?|5nl-p%)E z-SO}wFWm!tA8_qE9-fT5vxq-^#kH)EgO%BAEJg^k#WAQ_rv_O|ov%lv z(T=r*+990oXjJrPoXqUidi8mu@AzO_+f=cfxF=dZH=|)QPT1Rr3QZ5SxEdaK=E+RI zXx;Q0X?0Ibs}oxDuMBBj{okW?09rj)g|uFptv0;;H%?HmMfAtK8us!xoOJivf5d26 ze_ALoaqXrb4>)4y*(N7XH;}LP>XVKqsWA{*DRcM3r4no?YZwEP1ER@>rW$Qz=h4#$^4j&}9KZkJzF( zs19Rw4s;qn#QfZtZM)L^Y9qaC?n$pNiu;z!AB?ZHb1SCK^_^nphHA*$N8UwmJZbUW zvgVeZ2DgSWb?2Q_Gr0%eobxVPi7A z93dbebc*?_X-5Wr>q&p7LfpoWx^7i@J1DRDZ6`A$()<3dm6t0AIaRHx0j*bS$1!Qp z(OB9|dBcD5is!9;d16$u)-&-Q7qAx$m#}ZiN8{x}8%tSd6mE&n#i@Tv-h;e*4gI73 z|8v84{Vz$kTFqkDm3-7jLS!*2^;6!y-)7I%-u;l1G3vE-^RJk{Vc>o5 zKIvlfRXO?F&U}V(!#cq!twsKL5B>5kzV|m=`$XcYn`_v4{A!GI@a!hGq2%bg*D+Xs z3g6SJetVJ!vqz;vzNZGh$C#*s7he~#Nq2))Cv=#eQtH2tdaS$sWM;{4P>%wuM|@rf z<4#I^ChFz&TV^cVI=yTg5xB1?dp1?pQ&8}HzEs1Z@#Uu3t(dIUP|k;QIOTQIVrX-H zII-KsSqYZ#bb*W1fNCAb&8$)?TyVPB#&a$^Z@tS|HZJEH2dGSzzd9ttRj~utCIP`s z4x7trv8ovVE#^AdG69jw*s|pG46{~Z%iO@6W=y62c4oY8YN6z^5IfehnjVCK3>D}u z7J+jLwSFvk1=$Er7v^4aMp^RZ;ZUJ&E~Mr!ZCOlDw1KkcZaQgxE~%HS>{#aIY@nTt zb8EcVoX9_=i7}`%92?2o#mG%PXL+SJ@Pj|rQLNuADHOv+*`d14LHZDMgC8DR!~g$! z2-|%7p`eWCTMjW@))I;d45+(3ZL|abl+T{@dgUw~XmdDc#JA1)Lt@2zZeqT(1o)y5 zzFvewe%kA*yzWYSZzFj7q0^qgb8z@A8o>B2{~MOCeBIWK6 zC%q4c@GgM26}+7ZJm(Wa*eMN@fw6v3eKW0 zoHU&(IK^qCrg0qL`-1gGG5TNYey={L)ExHe+zQ=AWMf#%)NZ@@F%O!S8@}`pYLXX}4e4J`*^h%l3SIZ!b8_J5OdV z@C-fn1TQudsW`m5s}#_F@D_iCxi8&?=NYA3FbCpl7Eq@Q;3Vu9XU?wAS;O=4ipf@l z)5yGUAd`fBUZMWHC%exVfm8oY#;2=uz4f^d0SApX{?>xC^Tm^1XBX;KRT;bTUKLp` zot>_@Uz*$sD=^9?-q8?EaMbO36s%HwH=DlIY_QPUK)J)%1s3f-nOPuR%dOMY{$5wC z6)-E#<0F0xbGod_BT%8^XqA5XW;`=6`IqxU1k(rYXBpZ7!2%pz&0(X~kkoIDf5ce$ z9=w2S%S6B39rc^WS2w!-{_lsp_TOEhI@=h_)hP1i_zk`!{ zht-d}^!zezPI6Bsc^IvC{SEBw;VlYo_2M-Vc@T)?8XnEuQ_ls(>aH-zh-3{|5BY~KjMQYIM2+0o54|J zb}PrrV{N3rdtPlb#P^i^qT?ds&4`5SQWNoTCqH?jlLVnpQ{ft&MwLF0 z0^-nQ)J=W2egkWG!Kp}BWc)9-gI?15HFm09`#Bas0YNGi%JHV;*ioAwAkV@pPGuHr z?99*lPW6@ffB5T9dh#C&#R40yM%t_9%Fie67=zNrk~2J6U~za8bjhmeA_vi z(kfS|2y-lXY9cu$P5oM-weWSPoc-h*Myn$+9?d^o9b$e+km9<&EDv#9^z6*^Zs>Ft zp30P_sM9C2>SSH1>e&4^CEB0zO5ts;I_2&Xd&KB4*Zlm1X}T_!g?d3> z2>ynmQ<-k@WosOMBpcXCB^Ag}p02^N%KR*DJal-OJp|vL>D<8JKHTtj++Que95u6> zqCsz|kkFUtL%gUVk}3?9x4b_J*Bv^HID%hT!RI)d=bSpIV<&&98~Cobp30Q`CePPc zJ#Fq)&F+|~>6c`%8<}uye#shfFbwCJvMW{wc)H{l%n7W(X)8G}P*GU$@HD((R^s-O zg%zBSk*s|+uIoPz4_qCO>k`tMNh>C8h-b!sA3*H!b#R)ub2T|JI5botPyAS1oY@WU3 z7G#+EQ+8)`>go&w!tt1P1RENnQv*)(wIR;W|6?4E+GJrD{1rGgJbN0+yMQ;U9N_JM zQ{RL}cppSE&R8Ef4cCP@nj(}Db@;TkV*chx3z+}exVKgC|LCR&$5U0=lOpp}eueU0Fc zP>+k?_o*HqHrmd%qW#k8f5pdCh0Y%#KR3wT;y#}5q^1?)dup0!<{RJ*#5=#;=!ZOG zO>$?Bxt@YIjW?oGPw7zC{uyh*?eHtqmG)|NdeBH1HLndsxW{t!plX+H@-Df9b;RYh z_1OCnI{RlupH_^Ezv;-5v?r4g4Lao=fgKm1$8uM}DC(IKGVF249H2IP`o$ zQhidgvdAWq;aIevEV&(7{=rk3Ug^5G#dOWrAYto>&N7#)h~RIW!2?!5HmR8mv=Mr} z51-2H6}=TkFSh@g=poID1aE0V>`05*0>%0sz>;ou6lR~t(4I96&Em&SWp;_?zyF)Q z{Wa6vKLJhk`^9ebjZd6%@ybJbHM6^ZH9627D=i0oQ+US$nyRk0PCLukW<{9qxR*PH2?ttj1%BOm;%P z{F&+7W|ZmFJRy>uw527^6VAbDlaq)$W>OgHq04m6{yKk(o*NVghbkkQT%?GGK6<5& zYOm&I;D9uuQ>xJLF>#v=G{hgbK8+mnk(fX3Agv$T z9i;Wihrh*W_qp>oc6w0VU@_60lbn<-1y|*BWoVB2W>Vk%g7@U3lfiML)BWrU9XhJ{ zPEpgEljcms&ObYqX`4kIZn8Rbw4DjA^{Gbrf!DkB zFQMsIy*HsP8@i ze&hdQ4@-Qv*X8=NX)IZ0AJe$b+lKB5eAoiDGkb8>(!X>|@c5D1X!U2o4xL@ydFEwy zpZZ#|)<9>^?o-~sGv+Jo0&6h|etL>?_s@33{xS37a?wKM(N&JcMRpf1Lv?nGUT8Oc zhqObZE8gs{BmAvU( zh!*Tx$pGRO^r%d(x~9XN^DJ`f52CT`_4nNcKDcgKq4(g_(HH^yk6lBe zndCnK6%2XSgP*?t6@Sh;9xvv0EOvky@Cex^3G}fL8W%agG3!UC?A(6prGJk2(r5*0 zRInBzlewZThSM139wg5N^80ng$#PY=6Ki6W+b%Ubud>kFwA7#Td+fX+mEv6ZXya#$~^ided4kL>c?`v!*A;77A0jteJ1|1O5TrCjREF zf*=1eb&{XJh6`%X;e@Wvd`P31mhO@$T}92AucYcYI50GLUCz9*ZN|n)>!)e|m)ZZ^ z+=DOnx;Gc!^wtEc*{W~2k8yPf9g6DoHmg&A;%usM{maDvOILPBju!hmZJog?Vz5Xg zxXtv|7?)rM3c2J`KLqqN)$A?yMtRcJ=nTj9LG}XLEp_DQY6tp}de(+&s>$-5Um{nLD>qS!^R zd*?Vy3v?YXuDz({L1fnQ)6R#y&c;^Kc4#qeO@uKCSQCyDvpIuG`#}+ZYd;IWwCr?b z-`GRi4(QJ%ZLh}e&#m4`+f=n?t2x2j9XV!`DVUbe3}cxa8eF04BivG$eIKD0Q=}>9 zIxUB73xi)5mTS@(5ghQNM~OOk^4lR4k6EpIN??oaw7a=?h(V_AZTzik<(ukHXNF~4 zb?q)6d+*@1QqsM4F0PqRE{L!K+wkv}nQYCmqjknuJaFvj?Rr>d>rTB^A&>~ZwM@UY zmWq)VKQGE1rrgdov?Uu%sv@VqZN^)FSIi!srWu5Hi&O7=3An6qt@#RF%LpoHPs#5F zUA~4!wNgHifAkomOz(Pq%3edi9}=FLR;PGE*H5d{JYvAqo6k+5VgQK_Kg0*Z!JaeY zjzqA-%`J_Crq9F2e=r^g+9TV-G|3T6T;pkV%tU5X&zsHz)C1lKc}2(LdAR880X_h{ zDFL5Ie|Q|cq7R*(P=6>osa^-a9GwN=)l5OBHiY*i>f=sLqzmmnSkmqL!P^6!?GxZ7 z_5MBJEoz^*-fEvcQ@~ZuPzcwa=G)KnG+rhUozxdtk10p{EqJD{^L{*~B$WU^0Q|Uv zyY*g!9jV3wo^g}2)H;U_YaLGLbTl={qcYY(qy59DGh0Ptr7I)pYqJ6PCE(f`%7 zsngy*W8ZDkE>62DQ#i$6WbT2}!ZY=X`C1Na3PruDOeCBmR7m)zle5Df7nDX_@Vp$k?5g@lT-Qt)kyT z`TVVKqMxZxqv|tsI%=PZe4Kj0owF&#?VlXC3tIhOMveyeVu}|RV@v#qDZIst)XN5B z*BgEq#^>-M)JsTfPMfj9kM)4li-!(J3->a;t@OZK_156uEXaDK*uP$|xLJw3vP+l0Nda1VF+m56}%02T_b9nZBZ*9)2;9VhnZUXLYy;=+ULilkp(7HGGtBlbvPrZI?UYCN`3|=44iSIW}nf3}A5?za116sfx z0e5dU?mDWHb!J2Duoc{mFEEyc8^((EcIGrm`C8c`ce{;_vn^><$LEY}RNLiR$&HWvHT4@%Oj%X)Wqn=!sG z{o-k7x4F{3FFsRYvXjNCmgHj5u%#2rZY3xCH9yJz_N~*I-SR^m{3o@y(Xn9K61hb| z(pSaoK6y3XX=;mt@1%(MIRI|xZzXk}6S)h1S4TFM8rp+L226rO@Y7k0!aDb(wjH9} z1^?%Crun^;dsEy$)w2&=r7p^K7b|!?z*+~>obwT-(7JRNZ83e+`OsxX+?a{impI^q z>#i^6tXF`gJKjEd`|Q08{P<e&*)VZ)>G?&!GtiZ%kTdBYg_FOjDHOTCExgS29v{X9(0pOVYK)0craUk2WJ8F=qy;CrWl*Yiomo$mYQ(C(=d z$F=UceMO6vDKxpEs8%Y!n-0EANMGZuByi`r3rXavg7HzCE&didk(@S5u1RaBfNM;x z58*p__HFL$P}%XQwu4vy>y!6Qe+U*@Hu(|$S(yqWx{f&_QpEeR&}Z80}^b3!p+ zc+M?;5GVG*Wm1}E`L~GgX!#?2_I_hDb3kqR+&`!-C*?CMWQYoGamv?-qjWsOF6h+$ z=BW7#rEW4hPOl{X*3SP*LnSdvoJyHVG5FowXs6F}>~;M(QJbg5+m~CL-!QJt)pw)N znD^VG7DH?5?{e1|(B8yaQE2nHy+voyx2em!|NC^>p)-Hce~(Tv^~u|S9_PKIPEYi< zpN`WLX}*q7R_b>~C-mFYd`v@Q0q}!7r(PXqa5`DW;>d@3C-LGf;0=J+t?}Ovp4W-A zZVJ}EygW1zaz|$c0pa1P=aZ)m1zPFiQHztF>aslb-Z!B;|K3QCqIz_Fhx0D)8_hKF z?5_rLI@?%pB)MxJG#kMi<~g;>XnOsIxn7yaMW{bo49*C2XZ;>yk!L?=BRhGW?BvqM zIzxH#!dcb7sV`sm?9#MpP%qIz$g6-ot1D1^Z8 zGX3iNQhV4R{$SMnXsNUDhhG0};o-Fefw!K&(udK_l#RN(C4R}u4t+%3u^7oFb{i9s zI|<(@F+cb=%c)tPmg~f^tJLI2h;A8o!dOJqhkGcyvvSm8wWYrOirNg_RMt@_g!HZX z59YL)*2s(Gy;L=t*(tvL&n)j)t{vUBLW}Au+a9L32{?DPSNajg-YWW7GUJboR@Bek zgyHpiLDjhx+?MM{Exujq>kd~MjYvMkT¥?96B6>rqzNOx~U3-M4x)(=L6;CoQk^ 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⋘pWqt(OPD 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 OBLA=cO`JQv?)HC+Bp+1( diff --git a/app/src/main/assets/bin/x86_64/7za b/app/src/main/assets/bin/x86_64/7za deleted file mode 100644 index 2824e07c3e7c86d47482586fd749f153170194e6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2711048 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ମ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 U6(y?L@Cexq)=lSaL6nhafUKqI0^e>25l9r?3 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 SwC4?_ibOQV*&;-ky)*>g( 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

qX|?bIs3Fb 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�ylbFg#}FK)regpe#|8w##?+drPcU7(i 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

R|i@-gSx;WyKYH198ih1Wp-Nz` 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%0Db!|isrBT%k!Kw)|kB#f4ehnb%l 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

GWPn->TDZO1ep>w@Las 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∈bt_+Y&z!ve)|uuD#;pge{4iP}NJP zv*wrZZ@#B^v(octmYd7|&GqKSnj@|^`}g!&Z-l8gkkbvW9T3aLdQH5OO1Ko0^R(BDWxt}wA%c9G+|Nuns+CGlSiLGFma|tjWqQEY zK2)+VP}Xzx0#tpNWw5d&hi84q8=N!vt7mA>C&a(rg4xUhRzp~EHT8ao%}KE25c83H zzdz>s@o*(ZriNMeF?QImnAUbou7hPC*RNn>7A)I)Xjz}iltH<&j(8pc^fJoBU)*X0Wy+yTb<2XP=+>KbYZXGlI(qb}P@@oK$pM@Q zf@d$H=7*(bxe9(I2?Y}{3P`RVVC`vXcz!9G0fFc0@xT$ZUPulKfX`6b-@{+aYB*mv zyeDA2mVCgK3jkoXQYgF6O!hSlbh7{@-_QavZDy?si9P$Lp|WR<*rPHzxN=o4IF!$1 zYgYb_tVJ6G)^=6gEt=e#C4V&Hoy*p8NNt-k$rvO*M)k+B11v>?$Jl}V@R_j#zVOvJ z(ZX)}*q4YgvI(;a?B5sU7U#7iM&g3mG_?L*xb;T(kJak7lr^ccl46`s=~$Oxwtkey zI(}4>C-S~3tL46bwfsQJ4>-Jh;PL05)BhACS}5z=z9*e9DvE!uS`!%poD#E z?;z%w1cd;o`OJ*=je5QZtWUIxp~$p8bRHLcmpS!k$ZnmhSa#qdlBRD$HjeZ+4IU=P zJIu|O1Q>I>y-IfbghL24Nbg|Nc$d{RN&hE{*Ir}@0ZPDtH zl)S08%gJ>6!oSvihe1p!z2Gg!&jMh ztW#z{e8|$j$i`uVS+dn!df?PSG_ian3?0~dYi9%5j_yiNG;*?O+oC6nmRGO`&$0b^ z;f2hyg=U*yD_888d~V}EWidw=PH>4_h80zB?5B0=k+I8n9|K zH1cjji`v()dozs2yHzGtwWuT#)r#HY-`5fzf!tyF(OPG<1wK;XJSpuhms@a+8)XA* zO?RBlR|5HTCvh>)ptcHl2hvsgI>VQEpYc!QJ64nDNhlk74S(Wbt>RN`03GL2^qp`Q zM`h6Uz?K~6-Yeui^^1?OUU4=j{<8ml{8Qyg4Fk84aTk>0{`tQhNQDiiV+DonfSm1!6^#U~*I@4(Uo{W`7zKNfZ9c$qeBQvW_rGNx3;*Ryi z6;H@mx0)p%n@c~}V|_qs270`q)Q}VQuiA->)k3mB79J}ouClt|?CJ3ox5m}}zew#0 zdYm?I(F1&(KyOdZ3kLxrXtquE(Lf5q40-gc4-(*BIciG{$TXk3z1CBm@g6PXjYCQn zFq!o&(@yrdi(Q;61Q_-z^Lr-J!x+C_UmId~n#L z7$g{JP)=tuF>o|z*!^1TQ)N6f3cv2`fv?RBd^l_1*^(vmN(NBB^pT6a^D9{^KOZMT{DgmB{VA z;hPn;Oho+?+cX|7E0Vw^vac680DY+)Q_YvWOF%nuD_SOZ8*w4xHol8_+#{NRxTD{~`qskF;hgWz@TnWkO(*@6Bur3coe3PTpy zwZE^T3t0hD{Hg?B!`4Wi8Ta>!b;>2sk1UXz#~ zhgzG#Ec@(83wTAy3xV6}PG=O%7M!9a;)6(kHXk?^OoS<-`O4wtrgu8$T&`%fQ}XL_ zQa-dU@d5HOCHl6qUFEJ$=O$-M;yGGKaj2I+eP5;Rf!#IXsL`+rv~+KZ8jUhOFfDXE zN)L}AwjQBxC;Ky~uqM}EQvkGuf8K{fb;w(LnUqx=>S(#_FppciqVGf_r$oy?dY|ed z*O>OhlEpqRuW?J{oXlW7A#ax(C_tCY-;!ESbOo~5>G75EOWu>=2M7Yzd3lmuf6uTL zyHE2E^l5cVrqy3btC~c%Y_zI-oa^g=t(p^z6kpiKIQBS?pw)vX>|-o?5+fOYJ#K>A ze;J=2CjS59lN;Yt_`E}^`?v8a75wM;Of~<-PhnX@2&pVK)_!81T{+%}9WUHe z-IqPJxG7)p=QGl4TF3#TtZCSFQ(l%TwI2VYwN3PMO7y9KG}a3tMaz$tDrhpZsJnA! z1D|EiEb237o|CtkRXtJbu3wyc6;~K*_iLcld?V(NXsG%N5N5Q+>lo(VjHAmi0nc2&#Vjde0!2%SfRIgvTV z=*u0i&$Gsh_2+yg8>tR7%Q&@z_r!-PPqv)9QN|~6KOb3lmE8K#4+LoB*2!5Ta@~j7 zBa)s^$Tz{bipb&T7~yaJ3q#gt5+r!(-UTuY*l$riwXR{f*Rk5M7KPfupSG1?*;~m{ zAD5`<$_?4Xx(Hj&B~^tgdZxZ8y{yvS)LX8xj_>IOD6A`%h0l1PZjuVpAk zBH*q+ft=3wHYU&`=cDL4CVJq6$h%mp)knJg(cTk`h7%Qt8$#Ac!4lj^p20LeWPO|H zi1|dFBEG;1X)ks#KO*)z$DTAeN7+mf*>5%Pr)bF3DUF6qy_uuq#AvT-bT<0Pdjy#c zPNVT{*h4@Szv4{lqD6uX*xqMQyB%?0COUmiN<$G=XIz;T1}e>?3n|`MZyEq$Rt=5B9#_YoF;%VICxQk#6P?$W!lL2dTz|U}N)3xZbzL zM;MotRA#ByLHjIBpK?*WIDrjnGn2SiAjDlpe&UmT?g%GelAAUgsXMWSFD^YD;Na?# zL&u2t7cc+lH4cM0!D>c|k?))}-rB+J3wt(B=kxXx#N_K8r&T6EnpL>`3R>JAjC&vZ zll1!?x$_wtGUa6@`(18Vux0exlxB(Y=`E49L6mK&eeWSP#0bAz*$ws9E-?Q5WK_Ls zl`ZO^%S7e-FfX`LtFlz@v@SEQYUiS?%3=w?WelC_<9{JndM}nd74l`g=7k~-izux5 zB^%#-)5cu1Y@g9^l>+D{wI8SHEP&!P#sqTAlFdr=XIl5(EFg)HjmUjBao3wA4=k1L zul*y((~f7u*RUlG{`Hmem1EFg_R^=*z}3wNJJcwoUs2_}A15bKC`x_rgN6q{AHtQOCzy1PD)?IQJ`) zC3V@%LZ7{G9)|0imN2|L{Ygrc$GLmVdHKP%>z^hq3 z1diIW5q?*&<$h|2FY5t>V8lADPBt&}oukDNu%WV!$y!fK2L@!C-X zVO5rbHJ=+Q+@*KdRB!ATl1cSFQ2+_0=L6vzN?uM?zkWk%%tBEYQ!)-p3##eM#dqQ) z8m)5{KQD^+Y<<7b-|GAR1Nu{j$o=RKVQN@^!VAwzX<8N&e%;%kXuTi$kBoR60u&i|hb-k(n6!;V1F-QnLNOvn^qzr5%unevgQD!XzXO9~OEsYoCHJGZxhFtu7- zrbZzWov;IXgZB!R)j5mS2tp%Kaf6TyWt+njIZAM*xALAV8K{WYy@_J}xceKG(Ft>u zk{T~Lz~o3^Jkk3^LkgSJdAd%M4&5S^r0OSL3JTJE`<51YX;l_-6K0FtnE(++10Is9 zO}^we?pSPqz@$(gT5pM93sUyT*lA)^3rd~y+h?;)9#is&%jA7ajlpAqBE#UF>r2?b zRf^`G4k*jB?Dg`Wmp}JkDZkHpS;7^u;8a_iG7C;Pml+{E5B_u~f1O1S63erd^Pnc# z4cG&?>fFEoMQiH3rMm~`bH(weoZb%V-~#15o~!|pGlTYBY97Z6cK<<+@w$Y^l#9l0 z{@e-OR%owsMqm*9y|<2yQm0xDj^%}ObFn;$_}%F)IaPugZATC*9TR@dUbNSFmS7T+ zOBrCy+UT79h(MV5t|EX6>`nGMV%}|s_9kzFP6c1FCG%MSq(3sd&IK;j^HjI(f!6$a zStAhZ`ZU&FVGT?*U1b-!OF$o%KmXzh-H#8T()0pXYpTvjOkidEP$hBD|Cjz?K#}f8 zYW}3`I@DVBARZIK{gNwS9-1HZAOAY%+%v!ut2|brEbbsL#r@!bh5Rp0Dd+{;?*20F zy;ZOfAJVv5AtObNrlpoTy1RAH!p|Fb44WIm~)@gc9N79JP2Y+ANv z?z4JK>j|kdtq+m@a2su8pz3&q+A+zl7fHOw*@?|RTHT&4#tJc<-rwn-f5`B;hI55g z_Eq!zvA-h={+5<77Q-?HJ8-$}YUu&F^ojiwA0%jI)1PR$dy^;f4g0nhlI*E&d``e~ zyK9>`NBfM}FZq_jw@c65EimkEMbE(!uJY9y=zWwW7Vef50r-0_9BaxUjACfl^OY@~ zCyY6LoT5#5$uH|`io%?=@Gz`Q+6D|B`=^60^Qtzg_hzwZXuXh-YNCRig~e0~ zzxGWn0M)o<@rmhCm{-WPJw-%EGg}YjI$ycXD{+PspQ%L`2$4PQ8O3~dTI7_SPp@XL z$0DDW)%c8MlX;tXQhmzs5fC~TR^m5SJmcU0X5tP$r`F{N`bqB7W3}l_{4D+68OOVX zY@>8O=PUAjRQfqJAN#~-);=KVont$v%bEvWM_<|FVbCkiybbTcKB5Qa8x5Now<7$aB_Y5w-pZrN&spnx0(IRfb3zmfW`vc>~Z{#9+efUEjM$Z2M zDwgiRoHHH?dfxhW@4K>2_p!rL_Dg*(kaz$P7#g$h?Gc%6ihpSrciYRR&adJ$2j%2h zw8d3}SU(Q=F_5u#KE4+Co&WxvW!91lHNv+A;)^&IpSb&Z>|+CuvcKYGV=#WXcm2si zgY2fo_^ysG%mWG59A7X1|C%0Ii9;*NZ8Urt$dKyMKocg9Lw-EWt5^0Ge18)}F9-j2 zsChG}LKPu4V6TFOpk1({Ri*?RhE{G{2-;)hIq+1^(*VqcaLKPX`(y9=!p9Iv`VsM( zY4Wq>o(xJic%%|JQ{#YpUwpxf)yALCxVj?Rd;0w6i4Xht`sbx!q;1lvk!@U1k&B@i zc{h7bmHRHFrsSVBeK3G@Fg1Vxl{iaiCB5y7ZjFM=#ACcv@`_y_ZhhC+ddL^s#-4_U z5A!RU>aR(Ny|e6V_`Y;&4ty+?t0FsBhEB8U$<59o{$)~+G+IF#G8u={lbk@zZq+r9r3F@kX{&zYGOmQcsA#iom#f<>QBvhaHt0Y z#-S}YCo|Id##1&* za@2%$SLYn{g;3?9)#58CV}cD9$*)S%P&DrC+OE9LwM;-n2zqQt#WM z>qsKgP_nsQe(Y$MdQ#_={hO5n~db-vL^{MMY+c}C-IepzAAdd|o$C|bTz6hi6P{9-;}G>Tjj zDBGl6Qu}CQrzjZo$&+1RQmuI8+quE`3HE;t%Q>Nm@8wa|^iQe8-(8KR!vz6*h2Sh& zevve$)c$e4%FK%Jn^LO>>_*Af#(#FqV>I5ft1KilEgeTo(3f-Ual%^fSHor3mx{%d zRqqRy?B<7A$0b%){doQt^S>yqk`iN0^bHl%z&QDP31$R+l)C#Z(^H90!GT^}^*plq zYz7ojjKj@EP;jLL=8ds7m+fW+z^6BR_Hb)^Y->?$3%6$}cgS8mwyfcO%Mo|T6h{|s z?7Cn+p;N=YI%my(*}_Ek9I`Ny-RYc<+SMMQN3ikR!u|%bN~F$F1#6e`QXjwttva8! zWwMtEB0x{>Zv{QA!IE9BNy7a4^|H)hveR2zm=L`s@ayx4gxE$Y`ejRwRHSVS0`^lf z5U#Dq&17-joC?cWt}<)-b?#3J-PV^T9^|d7KmQcLCk3aR$1YcS3W}ZqB6SR#)en67 zoVIfXyVE-pt0}0*JJw#5+2Ld#LMegV+L&1R%KHMpdK4cK&H(C<&glJ9Ghe2v)3#U5 ztsXjO2x+U#r;vHe#>_n5u~P7rJ0GxC+HnG&fP`_TJYGnQDLQE=t% zU(%td3Y}-wymswGhFp1ABVUq6ga~CX-P@)1RWnr0_%7<#X)xd~iXRx=txV64pn#f-dtnypwD0 z2gMFz&?fp^V=Z{mTYFWR_>Bq6>3)V)?kx2%ze1FE4N5O4BCR!ly#41Esoes7xgk(jP13B05^{COD;@AT*JSJ__dpL@gfZV?}66esqj=$N7`=E}>lEk%}rfxN8o3KK9PRlm@O zA_}dfhbkBW=|kDe^{1qeWZc-OKv}*@{q$W1&%NFYOO|8qdY?J2UXE-%kp-eND@S~B z7sY#4^qVZka#?RvJI&tasMIXDzD(Gr+-{P^A;l;~$cyc&x>XLCZ1@KSsxOAoUmE8ZNoA$m>?cu!_459lir7gA8uR|;QZ zTO^!;71ZW>U~5yLd{Cq!n#NJ7*=~~I3t=(@2N-=@X zk-d>!S?tFu*}kAig(4=JK-t1VjKXngDce9$n9av+^IQAH2ymH<38U$YTx-S_iiEI) z6&l?H!Iy;|NWl&6l7VyWn&K0mxGcE9E5nbcoXiZ<1#8lx21r&?h#7dc-CJ~SHN5qOtw68tI2 zDP_F|-{w3GBz=vRy_@>`BDV>_vVvnyGi2`)j7euWCZ~k(9rJ)T_4P2P!y$Gj|uLvsF5Az53*?zt&?#iuQH3a+OWLa&yPQLI&Pc zeIv5!I}ewZ%zi7LUEc>?(yV^v);FK}oR@f?*b5xavy!?-XfZvoEZI4UN-A|8P{kDo zN_-VC>zp8Gkb#ot5@JpuD)wMnZNyFbI%CJ_>MOyr_e>+SSxsbj zl}6>Sb!~E{j@)~|6K1~~aq67&i6I~>umZ7hmW+zj%@HdhV8@dAH8Td$cS6OLm& zr8?;C`&22X^kVid#; zi8F4^l~^NMzUTST>vg^q{iIJo>?JHv^(+gAoPBq5a!0@lWD@2fuaT(|Q*YyCzxJhm zUiYC#SGqvnkz4k>G^|E}C_Vr{^CEl~C%>bwp#wE<%afgGlH z_7<#>qkXoW+-HD>uLB{qSQgHb?Ez1F|2D?SkdD4GKvh8&50lXm%A0QTi=7WMHVPlDfcP zhf*Ni3BX!jc1Ow6k@ybpTtE$y{q{rp{6i5=ifSMNhF7V|NQ3r9pyhE4EUk3`%kN1qF^ddjEy~k3i+{LV{D~VHhvkGy+PQp%sVg|p8$bdhixuBCVC$`| z2zvIZXiI8&UiFHg0&raOw&Y7${#h6jzLae~Sm->YW^_uUZ~@J!hEm>g^GBois?c=f z?m#rd5rrIC;R&bGpzs44N-wyXl%fY2U(@y$tRNoB+Jd(pAEwU)m^o@a>8?OAz0kDR zh@uk@3Gw!Rh@{AAkMpbD9kS`q`_`>0*2RTHje3Ek{A=^Jx+z)6qVg)^@UTwlP{qx{ zxJf<%#tgu4^KsntN85Y7sxv$c9z4tPUdL0W9`BWI-OIg~l8WKn{~Jz8E+9QC5&D*F zo1tUwzPUjIn_MGPCeyw;MbgUj#J8al=UB=lWbZBc)PjnxAt14S+;tTT&)DA5plGA) zOJ|2lUL-h)(J+i1lB_0HpR07tIg|5Edl-iHCUNM6!aoSu*USvri%srJ43s>JYgVYF zCsfvXbycXOlko8ag-tf)!Eu$#`u!vV*5XNUu+crLGl$XDU}-vUXr*E!=}2l+C*3lvWmui#lN!FUB{8)OytUTI$Rpj?=D zG^EP9B}cckm6*#7SkU%sbaPe4A_^qiGfv`XP?omMl-4_JV zFo*+7i7UwrlborxDbhi-z_p1fFnz6@l`{)0n!F3xQ3+>7#TMIJk*&#sIUSVfjtc*6 z)$P)P^XddSBRXxZ97tvSS-n5GnQ&9uMPQWZ6U2M(5M!JO~pS-1gJzFGxm@ql1x zjX9P?_ok@PCRb=YLUA9{4wA1dgSWw@I`4E>^Bi4#q zgKf*@rj-lfg|YBymRI%wYeRJR5^FdU{xTXhBhJB#yQe<+EGEb z)aLzev&&&9N0r_m@GQjr7*BF^ds^L}sJ>nji>VLfa@f#Tu_>!~lR2h=#>k>c9`Ys<{Nqx9a zU{n0ti7|f3JL;Tc?mTS&);%y9N%>_Z?q@P(tw*W-Av3F%*^;*D>EUcAidQ_R$>{@k zoJvtLirT#*XK>VvBV<^MS~zBldtd62DT*i}+n`Xp*;d^tC~`vIlm*NChlhoCl)o1r zE>rRh*MB} ziPd!AAmLhXs$XkERzpErOs7m!5QNXSLNbyxDT7lfaspXv+{2X7Im;?O}~i+n1_Mi7j8JnM)0|s#!C5lG8(K zvZnE?%DDMO#uFWH9zW3;JX1nBLiiDfnGDiY&}1 zQeAD@D&?ztY~HCJR}^y6ukoS!t6GsMw4T;7ZAx7!Wd)m01QZoNL=He@V~M5XvcFNl z9>8%|xD>k)&OT%OVqF`JF05w(WvikL9pV!b2z#a3lVz3u|-2Yehr1 z&=few0H-^im5gVlPXWtVat?$bo;d}&lOr2FT9q?tO8jz6H!2nLMhtP>Gvugzb%qNw zlXDMIX6YlIaRzQt1K=bidKfSN!Fd03{A&xpe-r<$(p`Mn(zs{n!2J_ItvomS z;$Lp86#Tc!=ahzV$i!><5uLGI)+u(##iAq-Ot^L`|HttE^%wp({4?=T5pn?3e;n+C z?l`=7_&6+SbH_m#eTM$oclKx&Rm7|>_1kRVdc%3EJ>la5R&52&k?o$??n8}R0@l1z zqk4ni%Gl>yp*?VD2mZ~rN~Zo#cgXV1Wa9&_xA5_ORG3`r%4dq-iqbEvukd#H0Ix5IScez20-gMx`n(A^0FY-;AgKJ$|YI9S>!@|wX z`1LXQaXWJc-7u|lOHJ$Q3X34=ivOGOKkKYFNmi9Zxa)lUI}~fuoeS-wgLLl3?K0-l zE8f_V9y>SrKPkRSs06Vi{0gxX$--wYmigWh>0M9FEc|2T_+q{4aEjfe*j=BML7k7( z7Rj1`85OhOa2fz<=)`V&MvrrHn~u-JMxO0o+M+?CDvRz_l=`>Q>wvYNJG?Pjn+6cv zPnfL9$O~%oEb%e|C2xfgf#wA4+dBoRR%fuJ%k|v(30Br;iZ1g4wP2kcQ1>=}=;5ZF z;($Giqkx_D@UDZwvQGm>&0A*d;D~Gfpl-!`E#TSNTX65MrQPb{yJ*J#11wxCs49e6vtD7)_&*vnEuL$bKm1NzJNVN zWqem|c`xe$nW#=Sa7aB4|^4d5&XofzU>Lc=T7o)Q?f7db8vL{d0u?T*qL{m5_D{(>W!(GH$2aHorw>EUp(@M?JB0mHE=4CQE<_fYdJ#tObWRZy4V-d+Jlh&kZp>8vv*0$~Bp+GUUFP<+Sl1!egHmh2 zIuE_()S{x#`mE_T#DVYD=TtdGt|2GcrumR$R(uH&t^au21>adR9E`|FY_7Q`XDQ~) ze>!Nk)m%7-X$-CLwRR1>%u;IB%dDC(v>?h?J}_D}#r!YjKjIIvGo3Nq z>%Nc>)s_yIbMK^!24_iNc-^&28~ihdE?>pZ5#_~#hZq09g70_J{{#5E!+?*8U2EC} zNi4eBynkQhZt@y;gwN6=kj1pJlJ?|qYnQLJ$2T~7nMK;kZ<>s0;CI-M?uhKx`w_d~ zVXFJTKR)1I%?CaHnf9zR?TO=oAWae7jIa-yz?+v;p%6vYk*MOIQiOF|3uDfaH9N4I zWcal}>aQh!SKgLjxG1QNNmA{Dpo1COh5!;98jG z2ifzFsRw)JykGcH_W)1$JFL31g>sKg=TFhDDMC|A1lGUMl>DQzBde#`cMkK6b!?VNZ55t{FjQ}*hwG3N)YCT^!!Av6`(W!NUOvAuVn3b$IE6{ z031Mh@!wCavm5^+Jlgfo*p|k>;?Hw9XluvD5XF0P6hLLo9}@*j^sJ-dHDxbf{fVas zBt~T%ureS5T9&^k_891K*JPCBX;oe);!~VB&a~fFEvg@|TD``37`xJF`7w8BHTGy% zC+DraHmxRKg}O{`(``5hwO`iF28CoX8t3xTw8jkQfjbpm#9~Vw^BjbIW1WUWY3q;C zqC84-f`5i}m9%WnD>BRG6&lO9Q!&TBU>^ndeqjjgO8nfgRQc_4}Ae=qIy(fkh==rfxCD($dZ4Tv{M z^bTeuI+eZV3ZnV==-d{(enH7e}Y^@ma)CN<||!!gV}~Jf0o4VN+}3A7LbVB%&##wOBME*@5F~m>uuf$&y-Z;*eH>zv*iKXdx-|2 zaS=eHC7i~i{A=@;yQzdkB~9bI&3lUbe!2H+q|oCVH=ZkHC0+$^5x>!`_46@r`?YFUCjs1ARR;XnLxy^1EtQV%HNhM@8v!1+)Ghs_bA%2 zDPHHCq>Q_w0(Od@imkWcQI5#Wxmk*QJ^Lq$Ib-(he{`fwn<2PBiP}6p`>ScmJtW&L z*q7P=V<W=+L;;I2`tfhF=FfkJ`^}$bUa0wVdI7`Lcm5otJoCrYW)G|!&}Y{Ck@G5< zKj%vo_LyN9p|yESd7xW*@|;EvA=a$*$o76!gOHj(29NR&nXq1``p=&a-1p19J4t2F zpTF|#Yz6Sl{E2y=;-x2XFO;92C-04rzA;Ze>^o1cdd!_?%Dy&UzC`MKg3O5@kg?9` z{iSHh`@o-?FEURMj>eo!k|_o0Bh0m*lqo0;*(dWXW}}U2S-?k zo5_-1hix9{PZ0AnwTR(%EHiP^51ENe=tN3x5dVpg7kXcv8~Kus10#n`Co|}o__R!d z$i(#AdJpx6o?CLyku0u@`Td9U>z$wXpI?&O8nd1E{npdC)C~VQY&}iK*A>5wK1^Y% zAS1J$YW;A{f1n>;hO!)~b|$z<$*+XcZsCb#_(o;Y=*lb-QNn24z+@1t8I7WujFx{s zk8VbeU+syAH-qAW^U8 zT@+BV;#Ul-l2J>{P~X(z$$A}EPt5Co%x8W=76*QulbB6P@s~DLO4T1NAB4Vz?&Zfo(QL72A2%hx-^*-0OupVfa6A_2apvt2a3Z3r+eaI2y<-lqR~?=;D6}13 zKbw*)e5?J3c!S%MXUop;RANQUPL2QxZh1CfYHNsxO<9lZ51Gojnf<2P)b*| zUxkRfSo_-9#425M*{f!JlF!6-)tLOOS<(rj>zp6rUC2R9rzoWf2#6V8dzC%CBM`3u zJ)6TNnCx|60!O=8t5$bmS%-4)PwQCq>{%V;l*s5^7^XQPYi41<^EyBIAe!PTA8_3(uCl2c+KT5^_Ei`{V|q+~_zX-S}BCOE&Cu`8lX1W5_D)(gK)Zqa;A z(WmyAge0_n*rj2`caHSEzkaEI{*C?Szeya`CBw9;Q|jwq9!y`JO^=+`^O%|M(eemh zN)U^8?T?kgkQNX0eH7p1+&X-0MkE+=J$WMIO{>5nu`%Xd#E&OkUz_(bc^0%;1$B}` z;{jz!c`c9nkPZqa99Q&cX;jYS3d;GU{EG#C?muO3$?o-d-iF68lw(+!M)Dy4+Pryg zibh$v!h_YucK_wlmh3k^ASM5he@U@EEq;&2Um%^>MvHy*5gPyAhukt0f51&rdlmY5 zCQ0A3u;QS7RVlNuLUblX$BYl5x7`Ch;StFG#Y&r=WdaM|D+=06z}NJ3_(@sS&zWLE zxfr9^5GJXotdZli7U9;dK@3Mj*5`7x!6HWXcyR1o9jb^cgVr#CDe#wt%u?&4I_LDq z6v<*Pi`i&fWvTRdYW%_?N^Dl)p5O-B`jy)pr|g!M>;l+MCE!=L*rDfuUU~V{SBOb$ z5bsk@3%(-Q6-)r3EV7p*h)!4}*>PPZG6z{XEs#9j4cWn)tkAajl;&OH|0g4{NX^XYHV@9x_IpPkQE-AA0P@(mtdKH?f}n1-{|?o zRkxxrgq?z$jmca4u1}3S)C`_(m>uSv9YRh=0QNEg_De?s*1~D={|K;Ze|IE!Z|)24 zbOo-!nX)hkpKRaQx9A>#+Zh7xr9XCYo5t*fEzWB@@#~2V9_v%6&7f0K#qwvo;It1q zAsC;#X+9!8SM`H0YexDdzA;B$XtOsUv{W+}xlP^LE}ryDcjXMmljQT9JZ61^(tFs` z($g1RHG2WfwQC^EgEE;tFH02SWgPj*d^Q?qQW;UHhs_O0+}m?`Vise5zQ4|i{88qu z_BxU{rc7F=Sj71zlL#)#6N=3A6A~Hj zo>t{}96)=+=b3Ro0#{{mRnu{N5GAOx2CI}CZ5RL@BPZqQ%^namawMa^?C+E)d#P_x znE__mR8@Q`-piW``^|RvW^^0CO!Fx*5k4}lSk(N9C`+VX4-|=N9vtA`u?v7a7CaMHt=62DS0O=(N=e zNhx9y{d4yw2<`@<3L%dR)IPa}7|=5cHUH)2I_`!Gmf$4#LN!%1M9CYp-tyyGBC!JI z`+_Ae%S~nbJMpwLZaPkCQ1BySEKw)+EyfMU@HXh#94PzLxZx@)Ae`)JArEc>H5}5< z$WMkfKIOP#kU-S%K7d|kT-hQq6^;WM49)gyp_lxHuy}@FH4Q)9$Mu9WYWrmf?*!e1 z|G+Q>`pfP`cPq1E0&ioxa>FgEPn&2h)h9-Lez7@@;3e6;2uR;#US@UmTY?F7`it) z$v35brQT*2t~EzJLZ-5HiZ}Z>lCq=n7v)yL_8x9dVfG^0OgcewiF{_Y#&-Rk3%ZUD zcO^gNxlese%0hCFP`Ufm$8Hf1sZmrroEo;|N)3^}#k$I3Jw6G8p9YEWdgG6BO+MpI z8Gz_7c4)hCIvk4pdKBe{Mn0s}q!T0x3-MqUdzGqE;IVNQ$PFK}4v-0+g(!AbGuIQV z*b-_3ux=|K46j+9L*Sch@Cp7~_yxJpV4DcF2R9*@SnoKeuTtYopAXZ2+5`R8zJ5)d z(_Ce3PPL*MsEBq)(~Z0=jck`jo|8s??KZOQjuii-ohVQ#d`dM;sGRjn`Qrkc{hgYZ zrt+jdXJ%L?mD-=ojZQitAEdHqiY2`^N6IK%3)`xng9+x$-JGYh;f4Fbtl=C6eUKc= zB?z9t(#>*a5QzP~C_HEGdG@3a^1U+idc(T{@k@~lJ_{^ulcR;?S2gp>+RvD=jrnqQ zL$K^%_z}}STh7LlW*cKhg?8209@ed%$g2T6k1M!a!X3RG$>-AWViI)abFio%GJi7^ z5??2mF+(W%suq>>0g(JNWAF8p^u(J4zzHp_i2HWIhpd&qte%X0 zx3n+KrVR&ygL?mQi!O?iB0o)NH(wIK!<%_dc8>X4{}xRYrbat($Q9Dt202~dDQy$ zKvX|+b!&WDp|SS$)dP%1Aw`(QR?j}au!+4Py9k+Re+QhW%M;s0;Mv+dKUdb;gPUPW z69Vz;qDA{W22)DSCRX&eyk;4IC!o6}GTTz~*jTRODG}t{t|$zljU(y@mSyJl56#xy zxvfraAU-%h;Mp+X=lNA-FGo(3Y*s6>wtGPAK(4-T34(I(iO<|88L&Z~ zXP@Ye0yY@l%Hu~vO z>#bxt{GAhF9#Y%}Z)xi-m*r^oX)HfLP1Oy1j0RcNtI&)AqF}@HyduQ}g?&r>GLLt$ zmXWcp{YFC%D7^|E_3-J@AVrUYdK9H{@B*sYKy}|3GoEH zy57hiQ+j^uE#bq-V^Z|(x4ADw4?=j9q`sg6O(*Yxu!asa*s-oJfSkU@+H#xW%GHtzHtCs&p`W}@r6!LI- z5l^O3_EDI34{EC!Eej0~x#7Inw9&cThtapZ8EYH8u7}R;Gg=2i;IWtv?sNEi>0@V=UhLf@Qrd@~JyKc`(YHRQ&=bj#GWn z@s``f%hJSTw~1?|i8Kn>3t$$+4&86P?7z@&J*OFE8$nw4`q+!fBw0n>G!MVv{>ca% zKf<4twbqYC<5Fe4>wK5K$yDW0Hnm**b<)p8*oaaL6P&_owtumfyw>+~C)lGgd?{H6IX_7UzH!xF$X z4p;d&L14Geju`gmkL{tWyB#@Cv+E0|#;?1lD8)3?n8`S%xaj<^lZA559ZWV1=48w1 z>I)LT`v&VC>qKf~f$GNs)!n}qH0brCuKI}z$H#Q06-_OQrXdeoh#u{M!@ww1^MBW70! z0@dwV{-8%uF@f0x`YNj@OU;M|T}Q~R4NZv{QLrnN#KRZ^ggG5eZ!gqNk)`g*tg_H1MM ztzeR?9{xYt-UL3X>TLX>?}QtJ{>APGnktQt@O_lVoQ1SO(EKqde0bIzSCXy5n$dw=hb z&xg#t=bq&`&w0*sp65BwdCqX+C`kA!xNUV zS12U#2{MIy|3v8UnYVlbnA}MK;SHbqcnzmMg5)2UpsI1_nnh4W1rqJq*flg+py(*F zi2GzQJ&=6cdRJWfmolZrYPsyl|3DVsN(aA}EUL5gK;Q+&U#Q`HuxZU8tiY0~fC5d( zckWG46T+SJElUUokTJYk1Ul~!DJk-@*_|Gw*c8Z}!izLVKu$SrTc!5btgj)6?z~`iCo9;gSiv?cBlHY~2~Jq&ZiN#d zP|A4@q{gulYGhVedn>q&&bda|L2fD!J9|nl2tVB1;#oeKh{|V;cwjN45cd%<%bzKyIr* zg)vU3Ib)AgR2P3JVl9#It3IK`tOsRYSrs*!e?TmI&WCqcpR2nFcW_IUfag=nkFmS& z&olOty^B(YDW0KdFk-8QjLa9ZC^)JbH8ncf2E-sJch8~YS)!2@kaa)eqebVSgsqB3 ztk2zI$!&?>hkFsG7wA!@Z~gXHv76+^QI?De{*p1TG0t>C zUz)DvPcy|CwP<)@BZ&c{j66OCS1e{5>H)SwACl1|*E09Tb9%97#+e)0{|~S08>G!dak%m% ztO3X0wyh&-SB$csQ!R?>VdvM#3^GE>I5PJF1$VMo^u%ry@rJmx}Fw zO`u1*>Kaj}Oy|CFr1(|Od2IKV*1ypUeHRi(1PLHjF8YQFKQ_x6_ z4*?3dS`vm)V0l5q;)C!r@~%1@5IkWcvs~w5Sp>V;PrCCzxo~i)Ad<^S$=$ujc&S#d z7K#caaOzeoxH2J`PK_p75p9jkJwjOevt2!0Pz0My6A2L%;GXPqgnu(HyF!oNKO}%P z{85$vL1~SFdO&`h`)VkMl`r3YXhZLS+8h^bUB)oY+?8|0K#Ve#L@DQo4d`E-H#jh} zp*)9=NO}scRfW<8r>2FvYX%81hkW{W@1`N?uuKE)M)V69gJ4e1;*m5<96{^ls$i;} zC>Kj9j~jUM0@G0wA-qEPddbGsqzimG$}(u$3i_G@RHOfDBaacP#sRLJ>Tp&x`Kf$P z#ql=McK}UCv-yB3EoMmK%xt}ot!s&jJG<=B4)<{)*}IK@lcK6Kl{JINhWl5m!jwvD zODVf$epu}tN36Q4#WJ_U)+=qPWB-mP87TjdmVD5%aC3iZ4OkNCiJ6`fNKHoptbjK{ zP^t4=PCq1C9eSg&tI2hhLDDgx_=D)EPkP)Kn@`x}NIfT0qL-W?Q&5`rVwpCkT_ckS z4py$(U1{>;_BlmJUp?bcgYD!^3FOFL=V8s}_>ANe)L7&7h-MIr91NHdG1{ z=~tFi=Z_YCQPNQ}K?Wey`juP;ODQyl&CArytCu|~v)+wziK2G`6X3lLi2Nj~9?;>FH8=Ip_Nnj(5;+GV47F0C^rHkRy~Y4*nA z%|C0s7XCX5j=ErQAiQGQ=+HFTlW1VlwNKZRjvjW@oCZYp)3PM_jMrFnBj_jF20xpg zn?1VGEDbwl&ptsnj-ngTf}qy@?3_@*E1>|wq$Jp(ho=k)3|KIgp<#c@NbixVQ_HX9 zJ&-uv{lPTye?*!)(qpB*imsW?vOsM|$BGH)U=l4?f zkgQD5DG)5!u0n!ZE0s0MT9QuzW<)MO-I*+MW@XvIKZhR>FQ>0;-Br-p_HXCh5_A4C zn#>~9KS%IIOU3bmH;rDG5y>~$`m?P4sX=`bnYePLdL zB~S+h#-2+q^0aOsKnybXjLGaz+hhN9qM#!0@BDzhH4#qwp5nydE>>f7_bzJ-PGgb7 z*gtXtG^!yrGRG#jb;RupM;57=aPKiVx>>b|uyz!DnGs*<%LmAMd+3pi~P9;#Y82a;fzhz$bR?L~uCwka>F>9rW`7vvc zL_J$53SivY%Qk4(xEB|&C33O{F{#5?DCce%cY)P((tQHL6@!iDVOydSQU4!&?dHm~ zgD@~grXK)661^n6j5GFY(zwREv@JQodxb&srg?v@J2t zuD+5L4M&O3OuKpukFnB?v$wL0l|z`(QclueP37X^{ukxq`L(pT9{(jd5Oofp2nxG| zQ4g5oQ_?W^&ym=W65^(BbndxYVOFs-kg%PxmrFn!Kj|t-vQ;UwICO~g2i*7OSK)qE ze_62G2_XdXBfR)|V+2l>g|s0hyJ)KE1|%T=(aPx4Qz_y84JnZYR2D1KA{cK-sVPj- zx|A#}WRaCe{_(BQf5bmKm&a)8_BN*3Djuul=s!VL&AOJHsC`?X+*$7V$bOt#Z)2oq zzmMrXh;cYa^z7Xo1=qsa)RHjEJ^Xc9a{i@+rSVUzdqEZ&&>@2_FQD_rgSijDPRTMR z{*Z6doFE{5KYhjvRb`7>@2W8M#VJ*hlM*Q-W{L|lzRV?evM4gsZ$C3mXeg2_6<2!U1d`x0kF(Q%a$}%9SUlywwIL}Mg92I zE)+&<6sP3Jv==JoXQkxFUMSMGl~OYI!g1blI2_zO>x z18wM#+l-J*ec6V>&l7x9Dn$TVNeYhaay&znpd&kez998m`Qj{*inK%xlZZKv)ad?% z*eqFb39lTkuT;XGFCTHiBvOa8vQmLzWF%hM%!ZsJMB;>b2E4`Sz*i_cgwL(gXJUYo zJLU^x5QY7;<}-IGHX}h|;uIi^NbN3h1)bbs9@fbEC4>?yeb-3MxE>Lx7{BOPF`C)U zY}`pJI1&+^^+mH`!8kI?KiQQe&iR5mDXBLlqs}7PzKmMG6w-`Zi`15?^?TtB;f6&@ zR-@#u&U!O0*_o;SRmc2VpM{`>g%08d=euvio^=FUHNz`zr}u`k1M2B0=2W0Kfi!E#B#t!VQPEEoTP<(?n63-` z?K0L|@1AzC3Phmu&`(d+=NtYB`WP|A9PE|CwN_j~LHDODgJQI;;(LSQ7NiEeHyLQ` z3QTUH)U1GQtY{nI&4q<$*XK?x-#@Lnl5g%T=CSuLp0232Lrph=b^=t^bH)9omRAqp+$msfd3IyDpdi_g0{R!OspO;Be8up5?I~Q2Y9~3fn*12$8^hPL7eD`etC_ z(1`Vy;o!r&R-9OTT|6~zAonU>yRAbDD=eo0?=-%Pyp6>}=7>9THT~A>vbR~GHcd7h zM=mkaD|la8ZlvD@HeoF}nk`w4a&5BWcWU+(8JRyo6mY{-Q+a9+1s6y`l5gf0-ADKr z^8MXZYlKpYDHeG%RAbQ9*WRNLh0tpILVqb7&#u2+P{NTdJ2a$BB_{Jwn^H>J2pAR3 zL?pll%+;UPgUwJ(xQ4!TEdOCZJo}bxA;mp9B2tukj@c@c+}r^f3xyeJb<0lPk2%pU z9$+T}KefxJfRX;k&lxFAiaueU;-rTu-fRiTd5todBTuvYeJ{LVKg=6R!-_{ zT0RsD`ZOyL%tE=+PTcyX<`J3~z&X^~rdP>sY=IB51a|Tn#b9=Yn$wD%d@5w4dki<3 z<$i>YRYqsFVZp$y6!G=J5X0NhvRd8Z+wgKqfaB^^?c2F->wA_L4!ji>rewB^$WMCksL;xma-tK@ri z_|@I_762Rzg9U{P2@K#KD+zZ176~_y0No11b6+PN@|0&U#b;S&i_|)%hA%3H^kex` zO}RG+-qJbN4+;&ro8FY-St6GtZ#}iF$QQUTVX-rrpQteFbN8lf+_sfmUgFG;)@1c|t^5qr9 z6FQlLx94k`($o&Rq>n3Rk=+b7xHHNXk=$RKhEg^$QeH2hh41+tgrY(PrQb(u?%;6u z(MroX4@SXzol306-Oa+dg1`pUfSHy}U+;PVk*=khF3~cX?xXjHV8|(}B&v&hDyZ~s zrxFsblZ0E|AYrN`^yq?J@N2{>>4TN+gLfj0bP>b~lsy?>VN6+W^ox@_f%dtPOc4vX zl${_ZTQ9q4x$Ge|QWqiqc(2n7lC~0&#r<>(IrovEvn^8DvXze}w@(G$4#Bxo_Be(1 zqm`@a4{ZyPWHUdYq@uD)X~C8jKBjc%g7x&QZ5@@*6+#RFBYg`m4XJZ^L4bj4|5nMY zc4($(sg}|**rEI9*t2*~e&W6}k%j5|7fA02uugnAOjBLcQ|%YpRkHk<&adPL6(>1( zIqWTf+S)(amh~^fZ(#`ckm$S0y|K(tB~SbZed4%B7JZRvkc`YD0hc*q%@y+bODy%WPOc@V_oM* zrG9^mj}fvcIGSb@O$6~v?2m_cVkj)F4rgp>go>#)f_yC|C>5VaoMYt31t*P&EIo>3 zwvFAeg{^Tsextl||2%)A)*fGEcc!;sPIAU>$@SGgu}#OF1!5u4>o#5x+?d=*sc_|T zWj{!qhUitYFn}-PqylZ}vaMxXV^o zvz6T9zHnf#fSu$JcYF+ufMYmAPsnxZ*eEQCR>^DE&g?6D+yfx z{rfriB7HN>ednZJ0buE)VAJe|+-<%(lUv|Ea|U`p z=}PZ0zW&~M%$b!hBQ17-;cBGWC4}6+o{1?yu*ohk$2P3yRLlHS`|(EAW{{9-{|OuE zu*+2Y5ToiZk;Gmj%QQR-&Cl#tTV6|dH&0ciSZY*9(4!g9k4S^|M~Hm2(%R;~CH z8vh)UxT?Zx2&Y~Nwhqej8$PNpr{nJ<4^XD{0;f=Sex2+ zkaIJYAS&xWH3$F0uM$to{=+hF3D7{F_^b(z5AQ;&*9xYqBlf)Km7F1+v|hkzrFW_Y zPtcvz7jrHvk_(RryP$Tw1e11eFd+9soh(;whz@P-U9g_al_d&rBRvOJE z(m94=yhv8Hl#g)Rlpk0kyx_x1KI61B%bkdJ+11NGfCb%4ae3R%Z2ux?V}-un;Jo8g zWQ12962k!!`RZ5fs5BlX(^s!Q`0UEIbh-{uM1jiK0_LO~bWS6^LlSF)V9Yy$8(KV*j-P5%ab#$W<>A zF{5e+b=X6^T_QfG=xh2GaI^}nGd()o<;4utm88Hix>uOEz^eD^DU)hHocLq(G5ow> zKb-nBsBXz!l-=y}__S$}&UEIjZKzmF{(w8TG!$t4h-#88t#x|6_%8|Yf6Q_e z@hRz_8EL2sxtNvu2+)2s@vNeQvftec0c`$7C;w&`1>~wBYFXUQV-NDKNs;0mr4Y&O zz-fn9ePi$wJmIV0UCf}#&B2MJPM27{r0Y2=97fneG1TLING*PIYHyQqTZL^fd&0y& z4pW(Gzt>35Wvo)|Zy1YC;K5wM!k!DOgBX8g8h>Wv=V07OK(>xTkz4_Xu*YmE2RHIp*qhI`?lS_i%F0a(9)n zj4QcG)?e1hYFQ+2OP0!3A}ebCOnabOK~r1jCy%M28g=j}8Rlf3+-dO|U=Un@#Yu%0 zfj@bdVdj8uGwjLpOHU`xi9JP6`a2Y!Ie3*r4BBQZ{-r$^Gx@xBy#~0q61U%ue@Q6B z_SQdMQQg(ng+w8db+?MP8m6d~wbh0rCok20km4469p^5E&0Q~}>g4U{Pp9vtJvx2M z6ot@7_;HUsw39xYw7mHpFuq*h1;!#d(U1|9ijEi=Ii%O5M27bPAt&hSRHyM z!aj9rU4(0@4D_0?d=1;sFufGncDeZIXNe!_T?L~Hk$)q$zl;ZMkIe25df^_NO3Kvv%)o^ z8T~rEyFIw>3Ty6Ab>|00{m5ap!M0$1q;&ml_jIB&vDS*WS=0AJ{=O!mw0Z3|AL4DT z4}rtK_XCILY8;+0S>f086Z)Gj$2qN!mKG&0Uz(qsxU@1kVkR3*N`gBlH`RVvqF?8ILlUoO z#u}QKRzwpCP2hLwCR}M^yoPg(e7@1AfBpF$`8-^IE|AZ&_2;Sj^H1gTxA1`O3Xjrv zOuMfYYM4j_3oWnmZd##UxEo~e&)|FOS)JdfuO2R4mqSb_1Wczw1OzR<;r-^K)I0*c z&T`wDu;e)fk1FCtbNB(JX1Q&6v)nOFG5hGwcN;)FbJbu)vWD2UI0f=g1Dod@;SFc6M z+w0l~rq-7qL7{+N59(Tmge?@^U>N|PJaqpDueW~?DUYd5Z@{FCgseEuCwTDGt z;I{J`c$I))VoO7&z|jWCj$i}|juIMn>t z$J66{YW<+0LRbehG4D&YqOGdp&6h>gNT4`a4lk zn&4VfLz3fJWl!C~?AUR6&)EUr@A5qfpY^|>k8zu^*2f;;p@%+Z4=LFt%RwWus1mgL zeJ)a)R>l5BVy86uG7`x5OOyX&xu~KhHEAGn>-VT*e-ZQ3{_0qHr#{vUnSP%>7NdVX z^|2=rHk3X#UV5nYv5T1eu>U)|^f44LR4&oMWJwsGn1zCdcGjBj>(Bq|`q*%=*`<$J zUAXl=hGzexJ|?W24do&*rZtpH$@%|U57X*bK3&xF zG^>9VFj2Jr^(FG&e)`uuK+I}hn<)ApG_S^EyZ7q_dZYXGXNvTY5{=9hXo1DVcKfk_LKihnC zmwx8Q)%xGk&%7%bu%1fQ7l7YG_0USyOx1T3r@?wu)xtfm#``%H^vn*U33y}4{@-g) zzmk>XURgPYeUH{O2mJ0wuv+LV%Sarg+Kam@PFPyVu9TyX;Zj+X9p9nW*e>QYP5BgsC4C>nUbgZD&03S zz)s$U--2&nrF)`Ojp(cVxMvv}-@ZPlG`14~LTPNjrR?`;Y&j?XCyi|vkba-WMuq=R zH8zpS_tV%)YL&JKV>&TWZ%uFej;4Cb^Fo*CY-rNc+qhxD$vg2IkmT!ZcZ^d=`UgMm zPdKs0=R*1H>uf*QpYN8>`FvK0Z@q(DJHIBEud`L?TpcNLZOiK}^of2kGvZqZ&@+^2 zeUef2Jc0qImJ7xe4rFHqcU+VoEc51NISbj2J4tSnsRWq;rOuw;HQc8Qk(Z#(o}Sxk zik#6`+36xUtEC&M(`pns#}Fl7W}Oz7Ga`ocU?83JrWBrRWXcHbP04=_BzM@uA~GY+ z^ir&-v?B-fDq7g$aINXv-Ce|-hQb@{_;!(;wf!2G^hQ7ELO#0zZYhBejP$jXKSIpg z0^wb}Sk2T#0EZzGg@@=n)*7kt!-Hsh`gVJsTe^nAEwZ;V?+%QIiM&+;kDH=N;Lx&! zXb@V?ko}4csrSXAttJ3*hkBT^HF~f+80ph#oHbj}lQ`bjBgFGqs}5otU=zpJ9u#kt zNd4hm<(RTFdnp@UaFf4zQOTQ9QDqZ?5RtUGmD_hFiXD5XvQZQ6vWS=Adf(Q$Ph})b zK__sM&|^fxsd5?E3X2$*9GH=rprbrHz+aKQix6f} znx0k=XF$pO>5uvB_xLMTj;@}}Bc}u+ATSqee5BvYa)5bs^3ks4m`D^o?e(U0lk`*X z(C}6I>F-ag9U77f6_nrUqLvZw27oS;fl!%|miSRZ=|<)nzIIYO*UR$SMe$jVDffc< zctI4Ipot+(54sQ1pLpwpQ?{1==YXRo%jonNn?~d@nSVhwqX!%5ditPNYq=DnqzM!Z z^arY3410--l>LxZB@9MOYP~8dxT0FdTEQ(8TB$(pwx^{fl^NvB?i0w+DtYy3-G10s zwI}~tqulh4Bb6j&~f=^w&8$R`SiecpY;CRLC>iBUd_LKHk$>hQSJCc=4If2mE zD%HYW5@25m5ljm={VYPnC8YRx*ZYgvXeR3boQqc2)+2{M$Bgug0OM2Le55B?sjEo> zrcJwmsX)VYx`ydT0@FY6s`m8<6(=Qey{c8*{5u&Nu|1lprDP-XR@tus50giZnzhwm53)b5Q_7P^(z%g?4-w!N(}YT%R!(`!MLY6~wYqGR25v3ksk zs52D~AOtq`|If_O9Kw5_PT)^^PCo2EyX89f`qb7-6307ci<$%Gt6O*1=LPZt++WX> zPqmj|Us4Z;;ZF_v^e6})fbtSI8dXoLuldQ3VGm+}0_u}zj@P!qq}n0;J2{k*Rr>*? z+9w)m*~~JkmdhZvV}#NV;p87@GO@*~SUM#)`C{t290vG7GKoRoWfkb`sOw;7i{pfvPP?JV$UH2BCTPmM!4uvN)>O>@op6-r ztLe0q#Y&Vg0Fyp#;&EoFMl|Lc-bywK7bX`{ct`77if{C2X9BhFCLlv!2t!;>yrW6* z*TcU154(}cshxgVZtu^!}UHS z9&VR+JzVO1i5e)qjwVDe0-?=C(PBiB`?qFN-RLhOB$y5RG%#CQb`W3Ec^Kw zQAq|h$VOkF+x`=7YedNl-Y&N!x4V!0lg4F7>7CSd1B0zAyVhs#0ysD89C?$&vQxeE zoZRFyf{nyt_NX3{jrR1dfN_e)@|TagRLzU`9<96li+E*-e1ZE>lz40T}4-0K{Ev z{!D2iDIE?_Zl}_0Wco0}FnLZ@?o0kS+68xh(h{$9DWp>T&0ppy9 z#pmM>;`8yF{i^>f{Q=sEKPy270XU?H z&s{T6jt%)UFe7ZmkMi;HD$OgQ=Rc-{HPCw?DZ7R9Pe}6 z^Z|4^HGg!k#3;2lNbHZ^sajbk7mEPC6h?j<{VLhWf$~m)}hJlTJm!>d5Mch}w zG3My@Ym?2bzw1V{-5$l`gotrKVgU(nJ0(ihkR&0{Z^fCf3e*{CnOpWGqL!$!;g&aX z*eN%bNI*&nG1e{ilCkK=WX`h2AMy<8^qmdqOpMEXTX8r=wI8FRBWJ2|Q!+B5DXma4 z<_?~4<`V$e=o4!?CW*v|iZt@w2EIk?0pZ;n2$D&Ro$Tl2`}V2BiT>FK;D_NL$|yCh zLW_a~=eb#&L>9+CQFwWFn4EZ==YkolKG7df(1`I&{F7w0-J3aLIHtffz(`+CYptIW z?z2(FCEJC=>X{^UaW8-T{W*KqG#P#SoZ%&J35~2F&QBL9{2gHIz7gtnIW zfrC5AZDHIflktj@_vunjVx;wMb{atOg4`Z6)u>uaRTm($Tu40eh_#+cP6fRP|FoX+ zU$_2M=p*Xfl&d(Tj9}v7t}6A|bsMocRsN!Kn|BMt<@1k*DTTDcmcgunj73G&z>)Tp z(WvCJMDVL)*uCDylKK+pMMbyEPi})&lUD%%^l~xv^oZ*B$^KFO7EK^amP$tR43)fp z9aJ)$xvbXtDLJNdP7ZweF18Gt07ZPL`X_$hx?RzY^iPv#!u~HhXDqt9v!Uc~y&1f8 zjh?il_PcRC%^FC$&)>w=&>uzyi6@J|M07=k(YSNoz+Lsdl6Y8#d0!oXr=&ZQm#FKPw=2x^n+AwZ~v|`;+#kwJ7_p;-!MGPDG=uQx8e+P ztwh)xxI*TJbZSU)hD;WY%f6@Pi<&FybXR8=2?X4I&Bw5P#&BqC0Ovu} z#?6Vz7TIJZhVmBSHsXlITGIl{QPbLLv%XMSBa5eRcDD}ElN!i0fHMTZ?ChStIeCBB z4nx8Tw#CCaQRfVZi?Cw~MZ!n6!UN^r=x~*U5dORuQ%Q|Fl_ck+w)V=-OBqbC^*!_Q z8mTMQvfm7T<$H1}`(}-p0KG5hKI>3Ln(^AgbA>%fyimLkU^FZc%9SQ;oiQ-GuJ`2@ z>%<=KaaF9GF-(PXg5E~;s_@;}H!DaL&N8q&pQ&@4k#Wf++zucRg`~*Bo>XT z0oFRXeOFC3O32c$W^|nIdYb*m?k^H2u&0oO34qf?l4S^382H6M85#Un(T z-ie;7Ms+nB=1%7?$`Az70^cp^JMxOwxR>Q zql8N<`jh!>;l%&B9C|nt*~%GvRY;D8)T@&T_Rs@Y9Y~d7W*4qMM}PNB1aTJoWkd85 zkFCMPfF?eP;|PY%xT(^X0xWkGqJ3w<_(>%35OLES?%riIKR zVg%>Bq&$xy$|F15;LwCLqt8vyj&86?HCN8bilzhsC*$raDY?3cKb-YoRLEFqzEWZVHS514wuK1?)m#b8?+<$c82P>9;e0*WIqYOmN=47%Ym<gZo=L4iRt3rExmSs&nBmOu|fkz3=X)fc(+11JkIdXwJ3fZ z)-tdP3Avv;`4;O71@1BcLORyRHtf8*9}LDr{Mcvs;RK73miy^aU*jZ_=ArZr<8Ft3 zgiGHs?o7*r*vkgv4{h8X=Cz{q1>??fJXl=g)4#aUe85gD%ppL!k>Y69TZgDxq7%ETkBvsC^C>ta-?l8 zK*<5tSNN55b+E1CwbCtyGf*Ku9 z-elaZ{0jFA6eERjgRq*FUwWs%mI8ie58#v2D@q%aQzNBo=A9ZQvYMc)gt4}^2r5h` zvM{Ih@E(T@y2+2JR*sLjIi+vR?_-a~7O_Lnlo4Aw!qS!0;?sd@ce_IQ%Yd&?E=CFv z?jM2B{78BB3=_Pd%NuFAD+d>OXnU>F+KL;kxB_ZIM)c8H?qg>QWz|FSMtU=u?RX>C zVp+W~1`RzzhywOM8()6B1Xw-R1p`A^FM)$8D1<4n0%+zK_DKWxqs8=T|?ZHdS z6GzKxYSSq%&L5E*p#SwnVe5W9rsYOvjQYU1WLvPNuT_=b;#Y1@UkllVt+X0g>}!gY z?o<_&i_cRf30)^i^Swi;v064>?v!gaz5cvjQLSbV=xB7$q+Wk6tv-agG@ExTRKug4 z_auLr4u4fY6^)+QHFW>Ai2X_@J~~gX=6i*V7XC{+xU5bkaWK?{Vbb7NE zf4N4t{UH&;d667HBnJxPmQHX8-#Y^*+U0)RcfYlI97Cxc( zM{ITeQy;{Ve;pGBL$C$PrDMXMZ4zwS@^K z(@E2-AKFeICU-CTKxU|}C8<1timdz8tS?V;n|Xv5R46?6ax&_}snA67>gPaxhq`Y} zx7pbUY$CbUFH_RJOyb}AzWRO!KTe;xg^Z7PmP$cV`NTE%>{z3jiv$P@cX%g74AWICJy6d-H5rzEidAeI=t6@>fGU^ zoji8{q64uHLT?qa1PQcO-WDFS^~k+Fb#4y4lEZoLw@HyXwZ_W2RP71o1Fg?6IW;|H z@!t@-meMdZ=JX}q>goD^n*W_PcMqeXuJv5T!wQ{1fPzkc*y@JR54!Td^4rVV;qh+&-fZV_XJ5WpxaBR+wyCGIuXA00+W z+UxO7-WlsxZyvUuZA*ft>@oYTPaZ)4iW2DqoVyJu%;NesGgxpuNi)!+y+8GqqjCsn zC0D>yMoJDR?-w`1b8;{{(8mi7QhaI#KW^qTR)3kV8s8^#jYD#1L+Hr^2P;cdmp#wq z@M+>;i2>2ki|zKu$$;SqIdFEnmyhp~Lduc3oWcv;%s1G`oPo$Xo#%1g3;Om7%mC+? z@fG|abLB0?T(aUawd#-kQ1I=PJ$SiB`=@L{^jM0e{&2V0wE@IJliRfTv;ABIq^|Iz zQ8~U@_B-l}Ilrjo+9>xSOf^i5^91IGObuZ~7=Fe-vxP@eYO^T~nMVJyA@f>S;I7Nd5Q zo5#wPEkrgVI3L}VcrjMmlvo#5s|Tt;Lj$zbQSw&j9R5FP-?YzwzeTyb^i{KTvuRA( ztM7%|8M8J!VokX&GtLc~GvdiCSw37w`ZH9NDkt zr}g0P;F^0(iejZ7B*rr?g-BtE&mcT{G$1?hQ)3Jmqu={7;UFz+?fGER%ut%q1ZStl zu^Pl|TNVW~m=S7?1)Umxrb2rkFelH2`Y#wVIB>qYy<4R6nC%G4h&&_xr4c#q=?GlZ zJuX|f`v`@5!W#=dD03hdTq5)@m$Er!7Zt!s#ve^tTTdzOy&23H*_DL?l0VP5LM|g4 z&J2!#NI(#&NO~o0tLh?l-nkS6Dj6pLl0iBXh%N%6vsHJwRkN5OMYb1Cyp0Mq+b5uta?oyY^I1> zr7|*4RGDQB7&`=Vx<6j%y5>3_SHQYcf=@ z9>L~zxR9*DbLl+s=&hVSvNfjV7uoJ304X~^u3TUyPF?O&H9#cvu4s`s6>|NVZgQ?; z)zx`9&TajI=4AjrVQBqAf?0q06$EXswcex9|E_>p5w%VG0C^5jTmkO^%fE+&91pGNR+*P^dp!&Ha7lVTqyCQTtzHTKIXbnD$*UB&%5b7AYC&Te89^$a`C4lTOm1!XUS7y*0DDp zH$+#8hG`aWa=)gux2IEI4L`3kFA&ir$5-|~1+?jAN7cWMgF~iNn8qDH- z-a3_E=m!><=sDO3z~AM^7w)nvAqyH`-9UMaWIN_|A;l zxtLH3jv|AR`I2#_;sRboc1S-=e$!r9q!yykzR@J7R*d3}ynhPutzn_OQHuOc3DdL} z$h$1Y=|9SMjz|&-SJEUx%?nh3Y&-vGTmY6!A=#`kG7w^5X4J|jEQ#DCAK}<)-ftx9 zp5X1TGbQih85YrIO1zp&?-BH1%(N@oq_dS&6R0YB)&{wkH*~Mj)MykI^LUuvxPyC+C-epOxvYY_qPmRp4DAb8MC1?7m z8@q!tMrJm=QE+fbcN{#JLt+;W0BD73pf*v8D)A4VTZd>qv`pcc@*WVE!{5Gs~WeCU#@^Et__{-Fpji%hG#kXnDWE0VDMkWG?1q zE(B?J_w5WKg~$-axxmu6Ycaspwe&VGYX$-5kssvW@nbH|zMWEPyoqPjIl#4dsWxeA zdb6;Qf*ZxB_O_Pwgl;|q=l;}?vhcI@m1?Esz07(*RWC|fY-_SyikrH18YN0>a;N^hId^rlgHZ4r4Zbd zy69Zqs4|fH#6Y%*7BeP`t|Sc=B%k$zw@c*BAXpI4VurO&8z-*4xJTv6l?F1+6$NAC z%Ei=~UsBsJd9=(INV;s)v4X2of!Pk-moKBT$fQ1T5hzIUUKI+FMUmGGa&@`5kNx%- zDSD(7-Jjx>i)RX z`@N8yQ`V$2YVh8+Ny~y&qcc3vJ%hmCnL%86si8_|Xm>BlqUs6tA@ACMV+nXmXw(_| zX0EaX%qRS``|&&bD3^CPV%`ebUf}a{`twBj+`wn|No3=weUHo^Z-d~+4m}%!7VHA| zC}!x}U;+NY`UT)>39ByISTzhzJAXW4MFeDsY&xQgpXZGxEqRjSL zne2coey8NWWSe`RLIkW`+`eS9N#}}S2;cjW`TFvKIWIi?0i-1hT0jJ<9&?2JKn6HA` zE!DJoi*(nfw0Gtd(&Hr7$-07FIrNzG_WYph*x^`d{q6l_0sV-S zf?G{He2LsJJwW1K6>O3$+fi)u5ls+6wakQ_r#e-(fN#NMF>+WXPqT{{hmO4cc+BS0kErb`5Op@Slb9)1*Wa~}e)*f;!H zDI%R|$bg1^A~h2wy>wloZZvSyRK7#`h(Bun zxXCl6Nq<_8kuhWqQd2`(lKVa_u?xcwaY=(pi`x_T5}^D|S?r7J0Ap!yo>EP4MN|8_ zG_5J=Xnpy=u#cs-kAq|C!)7L%C~(9D%rsc}qLlDFClsSjv6M-4qNyZtCo>n&z7}2Z zAtd!pv~&VH1$yZYS-of;?=~T)(PCMSc@hajuQMuOCbzO5r-Xz|y^$HflBELG$AT>) zm}HKTgw#txRX)yj=W*+5E(YYwNz$WWMQ~#})87!&5DWS-d6|{J>1z znv1`}AtIU_jbL&kM7I-DDF*U-WJ!a;I?1RS130OVgE41zesV~x_wg#kbQv8cFco1A z12yz12Ta$*t*>O2u}?3GvEDeb1By&`R~AYP7}0y9#Z{trSDQ|iDp49*`J*9$#I0Jb ziGCIYWpD19BoK*%e(dNW>K?f1%iPB?Be9S!^S0Bf;tyi(6+40$ddG*#f+AyVq1LS2 z(bHt{xG=ZW+Hu_s7XCWk5E=^%^3Rj|7qa{%Zi7SXn`r8z_SpDi-wiP19>c`NC>(av zpQWo1?Tx>YNTx+>68Q7MdNolMw-Y1j=%#3D-%pIYq)NMBKE*NoU!_m*o$5>&qpp=8 zcqvjsjXQd~l7_$@MXRI5??j9LAb65Nm9g}GBD1z}Fs(7m}D>W5pkH8(Va{nOYnotqN+iL$7rV64fv*8$n7f6KJ*i zx%@bgl!f`RR$W#rXm>Fx0)3%3j&g!Kub!m+U>+qfJ9%eZ$ZGRL84zb|yq8*xZaAEOImdDMo0-|i6lUkE^(WbIP$wC*6I?UUO)*UTiWn~s*TxF;wz4C@7Kn? zy2Nu*LIPG%;#pl{v6NV|e~IKwAaW$PoXIA)rE}kL2Dz6=ZYSBGRt509jCt)o1fDsE zE%b{ZXvZJByrhtVMWyu=!8+pa{dX87_JML)6#x_|o=0!ghUhUJb65RSz-FvDA-q=|@h zudGQBy7nV~zB^@S$oo^-cZo5o_*65ZvazsCrifD(mps+v;rUW|PW$;4JkD7o;Y!@& z2~-^gzy6YtBE(%3u)Jd^ulNe~9Vr*1-2V8~Cm(gBWCZZsnf4*LiXKr}_75bGKqBE* z3C;V#$t|!xqg`~XN-#pW?`_{OvoFmISR`9i=Id}glFTfcTi|Ui?kc}@Yi%~avTv#VA?Qb4vHAsT{5bA$xkZFQi72C1 zzVT)CUk`#6&t$HztEdIRw_>%Q#_}Wkxoe}T58F?7h8`I$-JI+f=OTpk7i#tTW`wb5 zQ9tF%oW7l3Sz)5G+TEaDDvD&4jh3!8QcFl+ecetg%GwSeVwz0Hd3=v4XJ-Q6#EQ3y zY2vOD^(9;sV_PKVM0$0I-Q}>_c(5_Lpq-4E-VRV%ZiWUM*#$Nh&XIM{E*POIZ#EV_ z!h4JxsS%ID`DSlrHI9iIrR~SPsknHLhz?QCxtjK={MTPf)uzFX_!p`Niu)^71e%LE&A`A-K?RW zIxfm|YhjKutvVw;ih7Auf%ofD!RT@YhBU)obV2Sn{UNm( z%Va?S^R@1JeA?_#t^j4nYgs&`;4)D(GEG*ft)1~sKO&e)l|8~s;4~-aAS3-Hm2$5~ zEO_W%*8XtnW5dWeU<2@?Lb!R)kyoaGP#9K#Sa9qHGODR~GP6F4;Lap%^W$wsttq*u zHbjohrGbDv6v{(1CG$03vLn_ibzSdNolWJ!mED)%<2~eO)6NU?(R&N&MCg_~sRZwh z-e{FT*Vp;TgT#vWnZZ}k)|bMym$^ak2nuRsVC9@GJbRhUN7Q^WgbTe>s3KK%>Fz(^t zii!NX&?!%{`lcE~Z0AOXLBUgU-OVVk!UepqQ2jN3whOlIVwqv;UH!S9dPJb3V(%*c zpf^{}t7RO2peT-l&r{H!hZVIkYv=MGf^J21yWiAQh?L(4SA}MUt*EsI**hP9lr=G9 z;ub}N_{Bg+HEw^^A1>S#v4bfOHFKX0b1YSM0w95^`GU;xhnDbWES${?=z0wKj~cq8 z>(`K!QY#@#5Uxqoz4d)HDoceZg*d$uD(KR`E6ak1AXXch<6(BVISW}+w${B*gQW-> zkWB9L1G;aGs{Uk5mHqZBKuewhSij+Ewh2fFeHWykbOVVIlv{?b-1!X;J~c zI_a;IgCCALMuyYKd%h9!hYI=uy=mqB21vNJN_A)#q5>WgR?ziV7AoLY(-_x(5bwLjou*J0uMr44X-91Fsj`c|5ZWn=jURgvPqW8B zJMRF4TKcfzOhuwVKlulfhvv3%D25s7ng1B{%FEd1E_MV;_4G4ge~Fr~JeP%z8X zanL40beq~wT_IaeYM0tr6qNNB8KiDoAG7Ra#0+E6yOLKW^&?j?NmUZ5+ByFM0+9Du z&!XI@urX#&E{Zu9UjQpJOHzq>=(`(|mn5Lyl|BB?2x41Q!L*!a< zN$t%y7JUT_G5a4%+b@0HSojg|ac4|^)IN<|dkkaI*Q7*CUosZ%ln47Hbw^j}W+StL zRXu9oRLB;0K1LwGjF+Cv(+_##92kasJSP3^@qW?LuZ?t@bl$qXC}v%pFP5Y96I972 zqiot6iw+i<2~&IBjy|L*d{j*a!4W5M@}t6mjQTF3S7XNDr5G z?K!z|`*zgFh8!c)D5J(UdW^``sUxIP@jocNrHHivh-9OqLH?{49|iG+0-lNdpEAl? zul6FMskOmZKdIXrVk&Z%Yrp4znTv!Ox2`XYTUX*$Rl)D%d|~GoGnHZBjXS`(Y(}2W zd#rS;k&^v8WuU7@YK%Iw2b*@REq7NP!q6}wPisA+KF~aVkZGOOmV3p-R1VkuallGA zyI02_u@>sE;tdtTx(OXNioZ2Q{D}s?jB|}mZq&JOWLr$ecn9-~ab+kbG5{MuNz5rY z1*oE_SRfbW;H;6z@aN-cx+xKgrp(+NS^02SFjHPHb9~;8&(QeB1Mq|+1Yo4Wyp0GK zjB@%pDvKD;4nO^hip*PyVPbH~Z5@EUmj3D}a;2{(agD4VBG;yw`AR>RVNIBLji?c|z?% z(wR~U!|enDIRR_WXmo}ilATpA0DUp%T6 zi{S;MCN~G<<*PN!&^Yu8xmZDmoCfJ zX@&UI6rva^4;B?LfDc6s#llaauF_2?XT~!dm2%b(6-V~aa3GXrQg5f7X5}1ihx!d3 z0!KSFUslYU!Oa-f3?$k0*1C>@;t#3Fs`D--y&87dG!cIHA@?w-^?N^kCu=vz=6gPy zPwdoA!Kd3HE(;#fMNv+{mRANq^`%1<5gF+mCa;hLSBBKEnB1Y~geI|x#*BB(Yj9uo_73Wn;(GcW$v9~-Kn0hm}m_>$J-7Wbjl~1 zYYv18FeVvQacXtWzg9?17E|a|ah86DT%U z$Gd}Nu3F7JIWHyGTQs`A5GjZ`BzXQKRoP?j&R52%bZ?*%dw@6yle?qf$*mgg+gR?p zXKW1NzS5mB3E_{ngYNk%f&52vrjKoKDc7B-CDN$<%8pp9YG!wI~ zh6Q~k*ohtg*eik^lJaBLQ|kGOh&B9lFGh37_d;s5aqBbL)>X{TJCN=WPi5@8h6ead zfeZY|`97qa^&JHPAL4PUiZs;=uIg6{A5x5C+hCZ7VV3TVIN`bYdTvhD-^OZ@6XEWS zh$vMTA!O+J8!Md*OX|ZI#GEUOqO!(Z16}l329|=d@Ty#rmzPTu{)$jFNQptZeQSgG zSDn=l|0=FS8M-NE9|dOGJX7Tyi>n-$_^?Q`rnYn z|8=Y%FXSXfc`pEn2EN;V9+4D^Vz$m>WGcuKE8RBRGt#XBF)K{xy0Q^e*DF-(MMpLl zWOac-kHU5(i%r~47RF1jC6I^(jF7?=0?K)S(jC6{PbM+q!$&5M0G;lb?7O08LvS^} zS77c9Lbp3q|L1o0zg=h)sWrY7cC`$N4>bi#0~ZguFDWLNY((L#Wv%#uH%QhipZzNZ zR4JlMy#V;7?To-u(=5F1x_lK$ipjHF5k%dN!Di_zP{AI7VUL30wk!;%b;1CJRXl+3 z`VcTEEl%Wbw*oj|mDRA%2hidk0v@H&iTT45v76IOsrufgbK9mi4kLuE_2Ig0p@_9H zVyD%ztPI_+Zmo7sk$XXA4{be351qK&2VpNr>w!q7KCuIq&jF41jEs0-S(r8D`|b_l z!x|&jhKQBEOBVUony{=LEYHrJI^jUyej2ekA6Azm@d|HaTQzKL49qql|@@2ybz2Dq@1nzAWa1YCnMkI1`xJm;r9jl_S7-jZs~Gx$(>{Wv;6GxR=>) zogIn}gny@A280{hO$<}QU;YUWz2kj4uiM&39DH|7~PQUZ{>4l<-U%%Sg(um``j(ekl(Y5gd5@xnlA~Q zfl#b*A|8=S6rIygMOQKgO)NqY=?xTS>B}+eWn)R5@l2Ch_dY|_s24U_dgG{7>;vMI z%1Ruyf7hSK$mc)nj{i)5epD*IQ-3ay&#k)jB>njsDg6sR_s}Pl zef=yy`bU0c*#$@)SWhHg9;VPO$YUJB;MQ|2<_k#69&_-|oc&lRUhF=fxF2x$uv!*x zk!)OKHFTu%dOJk2*i+kyIACP%W+$CLA-kqXRJi*JV0P?vz z{&PW0Jq5J9rWBal1uf1kknjeO(jfd@JSZiOqI%5U&)iOezMq++)zy6BUh&X!#D*)3 z+iQ0k*EjMY_lGxMVO&~!g>ijtKwQdFwWCqDTKL~4?q%DrFgB^e;$PAwZ-##TLbra* zShHFhb1xLJiXetD)#Y;4BfTHSMi`1Vy6izvm_)#^SF0TXQU6p#AvPN^kEA|^L2oiL zUjVIK;axEvILGV+%}W3(L0a{t_`0O&>~4JcG3fv}sRAEQFMb+7sImHJjn< zThulkJ`-3E7s}q3K3 zv(YQ?AD{?&0w1h>RdoXj6oVEmUXL0T6*qg?=R%e0#~LXo zdvXSF)DoDcB63g5_a>>rc@HyjS76bkp|QQTs=1v0fcB1gR&nQyd@f`6mCql%uRfnS zj;Wr<=N|l3)`xy=#^O5}o9P@v8Kx8pq4IV?iCW9b8IRaOLI&Y>a0j6a z3+mSlqT1bqTVyQan)-wdua6b8W4L$3+{^T~x>o+h-JWrZ+BdhwaJ2#^0J1nAVYa0{ zL8jUY9Y%V!VTBcCwzw@;yv}3{WlpW{6}4DVMY>vBC@0!3h*^zlpMDJjBhx;}r1=EW z*#dB}ey(g&iW@;J_?dVPxQ#5Zif68KSH7u$-6A!?=%t<+0@-)=`8xR!8L=~WwJ1tj zH+?{@m;QMR>uZLLHS!pgS-z%fC1?xvbI%tnU>S;&kiSn zJ6dps7|MHP**hOeP3nAvT5?Y#wcGjai{V{JozTDyZOGv%co57zBmOORs825uA-A?8 zsSb*ka+;tyYE0e~E&U3UFfz(En0qrvczZKcryErw{i)-K&eVNapb#Wzcu>$7lml6{ zhm1wHnjOmr9Mf~O`qLQNSL4?Rx+wYpOJ&r# zi`iHFn~tElZ_M81h3rH}OV_ch)TWk5>FTcf91K!q5C`N1pGeNw_#=(X0S3;q+tdUnFGng{nAs-35(-ODS-`My-XwpH9;9=H9p=2K%?v|PUxR+2|rqEWF9Ab zW!dL?nLd;Z-bwt?`ityx>~c3D-O33MXamFB`L<$szm|x!!=LQw|0Cy(E6e6}z#5Z>$YgO3ZeYKHjiW@4;8%yO zD~GBze~|pseW_A$Me*^#DOMa;i>M`eHgjq2o3fkoLFXWLn~z=Yy#tc?5&l-(h2>D@ zCtxX8AxNM5!Ib9)=x3{cM3D8PZXd@Cw_ils@4Dz&!Q%r$%iq2+C@_tt1*EP_J=(A0 zg2LeZBfGWTD!%2HQM-KjK@UJ6#`oND0>>+QyyPpej3u{*);pN5T}`R;+j@>QgbpE> z{6l3|2YCmW4eA67L=Hm>efh2a%n(%oH$JD`cOtt1-R-eKRft%|@eaG-8EP;aDo24t z`NwA3!;)fE7m;H6=RQl@l1&Yv`F>-cz)`d|p(Zq2!t6mgv-@vZ*qdz^7B!uJZF9q*6mV^{#9xEQ`E5W~h4I=l#5S zD)kMWx;2|Br{!k3Yjo<{EB(9&s?^7I>bh)Rg36QkS30#hn>tLTF3_pJ$-*mZ2=|Xm zsvWv;6aAOd7G)i{>&_V|x=mMzmC2BO5%kW%PaA(E?!1yv?yGf=-#EKhAYGe0h*}P$ z7D7K9&X1yTEA;m3g97V9hf*Zi`uADxyq7ri*PY7s{B=T+vhZ?SJLE$(VnArh0-@wdrJ*g2&ZiMkKvm$!Mm&roy}*pZPO+?ygw`1 zd6|^exP>3?l_Nh73ygM3=BQd9mdm?3*P_xAzYy;8)gHxtjJsuN%B~YKRimjF*g#5; z6y+*P+^Ed?jE}f=wi6KeUk-wyo(pyELJwDg0`;3IoTUSO=8tenOK3$(Oj@Gpf z6!(u3lvRRQ-3YYaas25%KceDh-+`7(2qO^Lf%g$ua*H-Fu2EMSEa$Qr-zW;5Fr;Ujz zvEchi*KwkBbLvn?CKS__me19}OX?z-gq%CdhU z5-=G!6~k=Qb}D9$axTCBEI~aBZ^se8YE5R;D0Y?p}kTX-iVJUp3%B{#DX5W1+-zgMzV;wHxVL zUNpi%Y>}Z(?q=;n@nCHfRJ427!w_Ztvx5YVP27~-IxcAzGFC$&w2-f9VW(*g5@Ncr zlaRjyPpT{z_=}81;_kRy24r;+e@focAz`!aBVv1tzmcYeLt-+awNnLnHOZjc{MP$Q zcAe_nmS5C~$x&;W$bvOG)nHpo+Au?^1rkgmOw(xk z&}(mBTD{iWhqboFYSB)B0Ko?!RWSGnppr3FKq>?(_E14-xREdbt6y>&kUV?hm))v z6odna9gOhDMp6FUr+H>x5n?=RI?tm1TdoO)4VN~JF>R{dtZh0&+Vop=8%m?18*G>M z+XcKsEn2_}e00etHvcpZ*4_i_=+0JJS=unEYjSXvXumKX+4Hh!39e~hP~~g5=wJ84 z&c{_TM+yluj1AwDcvez6;LULnRTmVon_I+guK)AC;33(b;Pe9FRwWmFw^?xE3*sKS zQXk8Uf+rOr|CflO;H#_zANl#|C`1F8nrZTDYZ(l8n}jN`h(CI%d1UiOVSR62%5_E^}sK#sakLF5Q`az*8L z5MwaH@*8Aa()mjGby`*$#J*tLN-i5{)!zlRNfDhvK&ztb!#Dm( z7l&p%PKSLW*EBK58L{P>hPw5NYo@qsN*glIc}c9hJI`|X5NDIkR5LNODI9^Kd~`3< z2^voS$r!Bq&qXYGow}|$8~S?bkAg<#J=8AgCAEi21NU>DxC%Qrho3aWyHAMsQ4*4A zqGWndGuiEMe4)m*S$DU=jd3V}(BBO zjmA9MR?0$L)jH^*LNK-U9dm(I%(L{oJw`m6tdm150j>Kay-cWeFViB?-r~Md&H~`) zFGzjHF?~N z*4+mf_qex|7H$AM%Ubpx?~HKR<|XK%Z4t2O7TxNgfAR4&vTDs0w{HPXHhHYN;m}?L zu8!|@C_ExN%@j$OM*p5hzrP_#qkl-F7t_LLe_ZJ_#nF zuY^Sajl#pdC+I`h$;rV*bP8*L6R2;pocr=NY#clBoky`6Jh8NJYiZ-c9NE-nUKgu~ zK}UTfUn?E?5vQ2o4S=aKN34CsBm&B@+RwGbvk(Kqk?85CGsnJa)!}8#9+1ixrNIyB z!#^7ac_|%8b zr>SN=0#kq(I?nvP;de4$Z~KQloAK8%nNfWiWf!ilhQfp ztWA?IL3u@IU~kPw#+WoYe1{4x3`d-x|obZFupY-20Re2qx$QBpI}ozSWohma%8; z+-C4E{4TsqJE#V-G3W6aK`?M4T+p8#{tB@X)m8#|BS!zM<(CO^z}w^j3? z3{VC{ld80rZ%%82LY`Cm-kL*{`t*q2I%0vzd5+3YWAl4qbl542=7?CoZ z*_y?bnWDwPOLixnIXh;7g$4NMqzzo9baP3=C2|J2+U1nm!s?i$gRCC8R631VM`dY!%t*A+VyF(9)0pRoX36*3hpHXed~&|yG*NhqXOCID?HR# zI=IX14R?M&+CHHd%EEvu$0CkDHUWZlV-E&-2|8!J9P=0M58w{^)^5`3|=@f!o3_G5$K|8()V zRg3eGeoAWB=I|k~C)&P}hS`|U?zigxKnvK22u^>oB={y8-(naShgfkBOXr!|^Dd#< z^*FiV>zH)C>z{|F2MMd4&E&NU-e-Mu5faDr(WN7C#ovmT3(dwajT8QPs}+}L*$ZRe zj45K=>IKh~j_k5sv)6X~=!tapOTIor&FCpR@TjZ4hUqVIA~Y@gcyjQ)lHeOawe)Ze zZTAln==5e1w(uKIy=pZGA9phw2&;*0n7ne(KDPHjad6^FG(_ntnCMDdW~IhqCrWzf zWIw*(KVSb<$f054N@R!vmjE$2*j3WpF`%e6G@5QP2JEt0HQxvE{Y%naYHlglCY$d= z`95F1V~SwiU<*uYH!z;wOWF z7dtjQN23BSh6_bdZX7mf-QcumpF?wqm~`VBl{g;a@a2x$AL&Nz`)i5*i;2F}L>Ec) ztrFd^IMmU}(G=kam}>Oxr<_R{exJrU^rQR^HjRFN5`=Daef&6MB>Qed14I@lQ{9GVq%j# zdF1IKc^WNGPs)>*(X~FRu)PQxxSbAjk%4KC)O$N9?7T1f{@_HAcq@&X*YPI%j6Lz8 zYXpH@PPi@*Ah_ogG0NAtxT#|U*r)Qxr775vR7`HN)`mK)wXGe&t1!@>_~g2)lx+sD zq7x?mdEHebC?G9()k)G~OiP9YuR4X`Di7~2mGdBxc-1o+{X4C4&7Z3FVgXXxc+s#B zJtOYxa!A|XzlFlFKXM8^bf$fc)on=iXZ@US#SJ(4i<);0ENcGez+w*G5B^jjM${_~ z{@d+5l?NyIn>%urPgux@JNVF^IN#Du<71%lZ@CCu zxxOHN`Bl(!w=xB$h=Uz}#`q;HdZl?CM}VLweFqQQ%L)h`GRk2(8zyjb)gWEkRC~Cq za%>|8gN?%%Kg87Py3?4K18jJlPhwIsE-u=RB;w-#qTIioSnZTwlB89g@L@eh{;xl< z0jhgZ$7Zcy~f`ajXy`?r_i-_eB%|r=S#=s;7(gCClnxG1s8t;xjaBrkRz&Q z7YT3gYMA~>m~$my!bV<3zCf~I6OL6Ki;8`7<30x*3*Tph=)qWA<0G58Db;wt( z!>T{a5)=D2%<6+D%7}2Ci(qaBHTw&kjg546F#HV$nEgGnzBckp?G?4ZXOT=z{sHqk z3ckxcUVBAIKFRyHzW+Gdjw}A`yf4S0hj;H&5}$o@`1W^M$k_O0{T(S=_<<>3HmXVx z!Gl}nw5XRb3kv=Nn6?Pv)jO zi;~~~S-3A`-NV<6Gds_85s~Y0IYJBPzQdO4Uh$uAJwGJ4p?F+4fDOig1sBwOt2@nq z?ph}76(>fM1zvaMpPZg?X86l=vkhMfJ5ilaD(1EKBk+lWCwQO;eUF0oH|;X%tid*T zXO*nWI8#V);uu7R#lpgzE&QU{qPQ^QvGH5@k2CUifmGc0&y+PLfG%ck?)2D=aJ6sw zU{!kwd?5qh`r>i>IGbtQ)&*D9+y#7BtYyQJcuB{$;~Nm2EB|CMu=zUh&FlC*X4Xy{ zRcoR0bnTw>&W8-Vr6d!08;0Hc4m#Av%(qWTOFOuE=?)LW$)5=TUcL@1B=SJfd0zjA z4Z&%|9BAq1-_?+L6Q2)m`gafz@{c0(-R!*{$SmgTz`&ZCcZFNrM|s5DE~IEpT*axm znTyyDc9E3smo*GO`CYC5mhlv^w#o0hzp-K1N4uz}S z9{yQ_?+WtufVW=)cxRmJhW8Qyv@x^!Lc4Sqy#Kz_1@ES7qwxN4qUJdwK`MA}5I`F< zcbw`f-@vs*0By|7iih_xwvqY%c?lB1dnqsxKlSni@SZ@Ed-U(;-0+SNK!NuHyL1=4 z&oCE6`uCx0T=2qc$n%l}so=dt00my5R%yMIZ{VFxgNezKK;g3c_Z|j5-#;%w0(ggA z15CvKwG4P$asu-jhJTKR9$Y0<9GG=rmB&9exJd?2dKwF!CmBPKn^jlM1KCE*LY@0w zzrbiYr-i}ZIDEyI?24L*zVkK{og0bH;)~tB11+Aw&qbq{S$Q*q`86(rr;fn8d{{ci zw0IJF%Hxu@3_}X=X5zsc+b967ps0J%(QvNR1JGdFUl$-i+C;jX*Ea{roRBWZq96Hy%Ftt~9EeuLci`vBwZj_^=BPbbq)DtX?5XQqUkDW zcs^&47!X~vOH>29J9d7`u7idBXP?%E{bDK+g>uJZr%-ysVmxrxbv#Qtxs!4mCoz>Y zY9Vw^7CCg$(DCeJa_IPyypo2WNd@6@P!YvI$+~;wtNcl&Uw2j7x?(WuvNhq;B!jzu z8iqabhSIAXwJq~|EWMTxnSfsJ;F_xr>^^A@XvbDMX$(ndy79~a`8Cfms?H**HOMw#X7x>XS{Ixmj&eBxHg)=6-k<|`@0Lp5Wp)IYYg@yrXj zllOD|Ct54cJZa)3f3JLP;wA5L9trJ0^JnU*U6*dEwX;>WmNtGBC1rEEY!@|89`};C zsQDV}LkH2KSrfMKAI4xXg4(l{6k8|ZQ|?-jp7g%GEHtAnmu{k#sy`*hCfM>bmyfs??E1E_Cp%nFAXU* zZT6WqcPzZWgPkF5D_4&Q7$xcJfI*WsXhw>a8&Qa|?*0d(euA~4crZGQx)#9#HSGAOmYp*=5^J@+haN%FjDt3fZp|$}+ZT=5Pyue%9|4#yTEV@;@6>Ox!!254FDFS{I6(a)Uh5nYH&)nr z^q(Z=e^A@3(dosPoYLQqR*^>4w%~ZmIc&eA5L82g_sQw6g+;|LYvJjl-7}`R_~l6h zmadT@>SEFK+2mlDZls(NYeEaXY07jLztG}n-`L%PHy}hJ-KJW z)%2vK%0vAc2WDYqkcC1lSBWUDE+f@!3g?C+RCj&VeAv5{9Wt?>fQoJ z9PGmXN#K1$Y5Jq0)4Ed3-+Dp@vO%7lAR$}=9uov-mZu|z8=TvysShaT0oPfkC$-}h^dc}rh-tx}Z> z(}^D^G+y`tr3pSh*B>os;XZcgme&oFvhA@M*v@4-QZG$}w^%DmFFOhuU4|3ij{cmE zgcFpt;=7rWqj>qF8u}V10d>7Nt@$vhfV=Rd<8|`5gO!V2O_`kjUXit8VK+Ymd>m7Q zCzF;}XUf3@E|V+^wv`mF6($k_w(Nz2xjFZoYsY1r7Z~M2TkAuDHf)d@Cw7(89^nv@ z3EBc2L7-M?fs}Nwjl}2E7_23YmuGb>ZK8A2*AZ#eil#z_savsW)gKgmFI^*aQySds z8sXSu1&?-artx{G$<`J%zmromXw8^VR6c5MXa3m7)>8VQO_Hn#UN?yIw|DapxWBaV zo86t?=X*A%?Jp}1?j(8h&JlxFd94*|o7)Et!WyQN?FHdvU9pJ5E(Ys{SV5%`g%P&A#x>Q#QmCfK_B6$rTHXhn9*$9}8j$kyg=W(wpr z82Vwab7W`B-zpG=b3-j;X$w|GgiT9P0IO0ng0THf@}o5R-^ zyPKI_wZ{&dq0?cKl;+ita9Z9&oQjZN<7@QuE^W~Yk5)M4M$7tOd${Gz?fY9^+J1y` zH+DA%{4HHo@%vLNq5h!*tQ8qqh3h3cR{yo%lK9dA9!5--$E0wxLMNUMFV6DzjOW%N z>IPYX`w2lx;J1W89%vzJE4i#Wl%GAv&dS+kz*dMs@$PnC?H+-6=y(Ma(8t@@Jz6nq zyp6@96}OJJQP&gPyQKs)eU;Dn3b)pF&si{+;%4P}YrDM*u9lMXD0#3VRZ9MXhn~5~ z%loZ)QfAqlmbfxe&`R%|4L#bZGC2%H;;-V)d}Qky;4G4q5qXFB(!3DWmga{*gdpZ} z{noDE+U;*XQ@j`Idfy{B#!5rF0bF@)RGCpFaV9DxJrFWzjM8y5zFp$ms z-M@uYtxNW1@t@5l;vQJ6hv!B4pYZF?`$YUZ2VpI+K9C2Y!4)b`V$54Bpgr^csSqBh ztD|mi?bhfqLYTewiIm|qs-)*h%=XDS28nll#8UKU-J>$Rg)HwN3=p#D* zt@PId8Pdfqp9mwMe@uLI{CCFb$DMeuJ^nja#`2G282_D1$@>4B@vjFplsENZ{D-mG zj@T3Qb^M3FshN_D|L_+z*6yF!@gE*bT!i2B9RJ~bcl@!A|M2UwM2Yg3|K;O93TUDy z=+%)AsxJOBlR~%f%23NczrU$$NA<5N^2+vq{fzNeM$12c(2OJ~_Xo!eH{Q#394)BG zF54gQMAKnTcbp0af-Xeh( zQQp67|II}8m#9|OIQ2g^MHmTVt&^@a?-NZwE$>KF$|&3Mv1gA}w&MfOo|)+yO-(!A z_3XKYfaX*9u7KyPdHvrid*n9HSv!xGJ$18Z)QhI3Ko<7f*hTU&;2G7-$B?YC{{az} zJ@s|Zs8!}mU{u*tw|Pdbw%-ckWCNH{9c={MSaD>v{qC?Iw!~uP2W{Zc@eT;IcG`Gg zh=MX}d<-nNj_(Z^j*_I49jW3GBULQ^npQF^uOD*7;wg%mn+#@#=4r))HNmtzQ}GuJ z4sXpfHI*eqn^nIS46Sa`9=*ot(ZHv)q*Ag9#ayG^DWsK&#d(Bep24{yhJ+wt8_{E33uSjH0l;)oK^uTB3s)-tr%+;cau>RbeY%G(>DBuu0} z5dlC1frg74u9TUd`5%|KabhN=^9%lKT=y;C&f=_0rgX8ZlGi+y0n!445DpL#n2Xi4 z%4fCuto2O%Mo?(#-mv5inZTLGTdnn*!<#$4Gc->$e}lTnQr`583IsU=tQ@P{@?#&g zwHcvdoM5Z!Dl4ZrFUQI$&Bqoh5|=J;+Gxy?SuIBjilIM8T$b^i!#Q_3Q(XmE_3Oa| zL)f#hjNdudGI=MRhjgrk{Ohfp8KbS7TQIirx0PGlU#C&4#oTT*>oZ3F!gEOMC9&5| zd>x6ei%lGuWcQzylaXiT49z!5rwKT-6r4!OfSXSHx3)H~&l$7c?$sy+*4E7|+8T>< z(uoogGcuI4f9#JDM2Kk45DTdzVEWM}1J@LRMeNA@Q-N#-jh`(Io+%JW)3<~8?d}%c z&N*ye6tP~;N~`)y;o;gZ9DR-WMXJbnA05wxKS25UveW2ZThu>V zoz-%*x-`FCWUU_dLz18J&WV+zT$8Tr(W#&MDnApg>)%YxrFretWBOl|^POz}VC!jr-qG@)KmX`*YqbvEi-V>4p9IH>cs+psQY7fZ6m^(p?~RTJ zd(d8tCzOv2U8`xsG^v$!R>-bK3w3@$gmy7z=;P+YB#1v*+-0s}_ET6kg%adx+mfvMyF2wW66`-fo%0&#hYJ9aMR= z+MoXrzpR_3dU47s+~<=M#M#PAqYXEI9oOn*8>zgsF(bER49C%ST**Ex%c0_QoYcN! z@Og(;?#~NJ3AGzf*V-RySyEA)$D;LRU-vpDI>twCjYA)#$mSotfUeMGaqg@Au%_im zHA;kP(G*mRE}(iIpJlCjxauX^`BiCtrI(+(JS|JA`|*%&NJXwS{Ft)lOMwf$oL87% z%~;@N$sBD*yDpx0wENOu7J4LN3WeDSluV--c2a-I9Dc5sx^MLGGm>)&sefao)GriM zLE`EvFF)0kTwQ#r5~~?h(f*V2J(wHdsu;MfB-qO3TX{^7FJF00=tw3L`&JihqoY79{Ekj|o?JP5AsALPJcuvs#YqhWd}}hWd}} zhWd}}{-tI+vioK_m)N~^SegT~OmadbezNKp=DCy=Nf{XcVJ<(+k z!0FOvH$3;5xBEhTOweX&Q|+o6h>B)`S$?Eh3`(JKQ)tz%$;hbTw^T>*G#$~iWPnu{ zGfag&KEy9`4ZpJ(%E~Ii`IUXL@`|BF>fZQ8ExJO611I9z2ciOc261w z5#H-D;R9Y1{%np3A4`*>m(X7(T;Vn0^K%G~v%gvv?_Q?WF5Z2gR=arjzi73KcmG_g zT}*#VHH+zQ6F#lgE~dY=TGQXJ$Pl2vMpre}A54 z`q<3B=cVgg%$(Ty)gJl~@sB#b`S${$CsUVrIw@(lL%jC<8>d_`>zV)l`B*LWBl0cW zwE8Z$1wHO4OXL37BiPQ%}HQzi4or`O$^>F_HQA%3IC+dmdrB|ED_t%FXqL z3`9BK>--x&fg0@f3G;6{rRe-CPOQ%SD-J+foIO7N-~9VOef|w!&X#r4|C@jRZ<&9? zrHANmU0=#V>4F+DNHF?NGe3ShGXF~aiLvpGL-R@-hvv%+f_lV*!TY}sy{hg}Wa%XC`+*|sWZ6=Cr z5|B*-vPp1f_v)I%HI-wvjF>vFH|5?KSFU6tN`f0DueY)gmKBlEuMhEdqcg!z^~n0> z`jGtI*iLFsF4;@sft8l8;bLDy2|k7#DN@qKw~Wf6 zXw6DkgBJx$KH)>Z%AE3si_05ISg4QXw2_jdd`z#*_)a!!_R@yT2bW^7C4SZ;GR)jf zU@e;fzO4~?)(F%xBOZ`RkRW6Nvo3kStV+`I#ClH27bVRrXB?pr}iOl0zC?e^`SX|EU38DM{!`3Ft}* zdZ&PWbl5ztF1JFf%dOJta;vk-t|yM_B%nG8s7?Z^lYr_ZpgIYtP68;$hjqoW*&)n{gcNKIWb{PQ1!&A_MPOJ6@_wHnJot30`GQL3O3$Hd=RI163&6()G8*1lRY| zw;TIwFgHVkuViZQ%>f$R$yR|WN3nagA&w~n^le6_2D39XV9Vi~(82tE8Z1oLV7$;F z|InjNn%}bt)h1h;h`lxu6DW3zOi*ID!1adL+-Nw@+<$74uN>Ckn};;Gv&(0w;5aJW z3=*kO0xFb%3MHUI38+v4DwKc=G?V`YM=(A*2Vq zN>2~em>^^VIa8h_a+Hs77ZZdC%Jv=gRWR)M70yl|{D43>`(od2xHrGTy$OVS6Wr3@ zxAcACNkY9EtTTaa2%~P%Pjo3tKBK>1OAkNBG^J3?HU0M$yvNp;)b6O6T!_hOb7|o! zt{sIU(&XR|2*U(5CJ31zO$1irOi*Kj5P`P12vVToB@hOIOcn%87Mp1@aS{+Gfy@>J zH?qdI;Rp)=(s>Gw4aa=pn>HMC3=!Pk9e$HjJfe%+6i<%)?&yDeu7ADO-M^D=-a%vK zW;N+mYuSyoLqDOWlMt3Dzoq%z&VK*#u7ACiSH;!U?o{8iu7>P}3-f|j@WOnmb(@Bv z$6mlzK+b|uqNL`?*!MFQaOKL8sFu2)XsP=#~>f6hM1s>r22aP%sM=%pMwR zZS^#R4z$)s<=sXLolY>BM+u{G+E_vgn#DvV z!HuS^BYjJoO&d2>nl_pNQ=tV^SDD6DYpv+WWfaBGhpigNO(Mz4ysQ}zqy9gx^>{7~ zGy3q~O=CTN(SgjL5vKcpqwDb!AWotRj}!^MME^}LU4QG{?6&@Uu#K?ltlsqAX!WhS zR^ecd-g_l4F1>e?qkk3tCR%J0)R-V-0yBTyYvzx8&HQn%nLqAzX}?9wY;@rv6QrGB z$}~ZZ2|@(gp)$U8Zjqqet8ep*M88_L10GB_@hbzEU>?x~6+|07sK_WHYvh=@hCub8 z=kZzfpbE}C_X^7#aj(84hV4ERcb``wy|2auA%dQ5bV$z`7)w`0NRP%sa?)4=8SDfy z8S3ybn_||oN+nc)Wh83pr8di$X0wo4HcMyQUE<+(hA&LB`NS-ke6sbsfg?%(neLDs zik^Km()5uDYD^F^LE0D-XM!3Nga~@#CenX8dR4=&PxpE~B%ce=s#VvgCq%KGqMys= z(%q^t*QfhTF?>@kxqkbdDaO!)T!$<4ASJu>;^GMKzndN+>-#T$BW8V%Irv0-6FP>U zMbdw!NBZG%v=`>_L;ORysV`gXZu{?l6pNfi7Dm?o?CvA`AN|C&^{f0mx%`63wdn7m z73OlIRX-)&em4Szdsa^%4;}OV`33H}9&zgKS(i(A(f#z1BC1@hqRKmoMU{k7ikZ)h z(MxpyT-#uzkpf#9ao|*Gq`;O&3fL1n%6%x84gcOZMgF+JmOns|${z)Y-L}-X@Yh8-`p^ zp+bkW&=xw(=6el*nf9sdK?6<8W{c=hVD{m)Eh=~v7{Q~!2p$E|{dm(pkva;hD>SQ- zItr>)>L^gDLra{JZc7`pdy_T_s?!h<3aWh~QQ?djF@?wj93yXBS!(P53Df6HdW0x2*O z2t&n;5hRf4e*KTNAR~H+MWGIPOnqbtB8*DJZ)j0P4_Rm#mS98=;Z^qfGMW4l)BZRM zi5idTFB8--M@#P!^a7hk=}x{rCG9U|EsnfVVD|L&o9^oClkV&5lOux^n7w`drn~$4 z6xrX`ZUmupweU}ZWA1+>l!A!-QegJ{^_%Yc>yz&L>ysnX6qvn#{ieJB`V_PO|NX>r z&O%4dk#1P{|0jA%EKLFfDDyIukq8*;r}bD#fsWZsqv%Hd#4~@cB56Wq^?cH zVtX5MDHNvVs=u(6lT8zIXd=xl9}SBNS%sEWI5Wp8yahM+d3@Rv5{<#DR_<46VH<<@S_kK8TR&bwz~?YmoSn9X!X_&do&__X9A z{Hw{fCnSbucP5_5ZZ;E{kmkjvl~cWpB6p*t;@Va8N|sd!wHJb*!dsLI$W^sL=}5-g zCK(w}^DL+>w>Uo}V;rKz?oOqW}I6^Y47^?nO8aGGSER zrsEy&!PVl>a5if^xl9Oe1wUR2LCc30x@&b4YeC!^(PaBkzQV7&H$*knoB`_3?-ot9 zjxnK5XOt^dO4-VlIeNpE!Ye027sYw){6f*SJGe4pAN-12Z1-9P*<^>u{N8&)TGdQ$ z&oox}&(e*s-eHvl%{vB^rnh$Q5H9#A{1f&vUkqD?^Wg}|g|A%Sla5|gid#6ET$c~V z=U|8BWNezqD9R1-J1h6elEOpWpIUi;^N!5o#*7l-rVnWejl(lDs55<4_fD$-_EEr` zQox+z9-nd+I=4@DRlPaU%Jt>&{BF|UX3Hvg z(JGu29ruxPTRI;D--+^L8cKfsZO%@f-yB<*p#EW+Uw@m2^^*TH!n_jXAFBEFw|QtU z`FF{nN|1ku=GWimA-&{(BuW0knqPmL2ltX+jxbGBKlh-6KmBbU)Jy))B*{Ne^XqT( zz+Uq2l6|%W@H59yzy3C7^^*UQB>4wue*JA8&`bU=C&{0w`SrIsvzPpzNs>Q9^XqSO zMlbny$>w7M`1@;q{cY~wOa4cav-?&3nFSw*?RkGseB%bAG`d5kEMlwR}QgDf8%+^%hoyYj9%LC%g%b0&w49gImj;kZHwb6sml+a z0e8SZET(^B+pipEm;ScJ@ti)Czlzb%TkuF9D0}jw#D)6K9t`tW%_O^alOilHE&wJqA+c$JqLn z4{y-lEyH@tKT3o@kw0VeE1%tp#92cHt6q`jNbCUC&E)t_?3@r(BCcnd&~b= z;{3`dHt6q`e!b-{Pn=)*zy|%@lHObXQ6|5xzngdVYdJKZL*lyD?7Z`akm_6So`h0r z&CbO)ggVc1$$OICPHU2V+9dmyMF$b*rU7`U@BB=m*=58 zaleu>)qjxZN!V>yPqN=xPnS%~O{=Q16HfBTEnc?c<12jhdgW&h@@tJ_&UKEjv8je? zxHhUCx5j?_PY@@^3F729fu!$?BX8w9qF;^_%flooYSJ8Sh{OM=|Mk)646AUv2Z5V8 z9CZJ$e8EkUNb-ZwZ-r1UbAQp{fs5H`&RG08oVKgSF6!@HK6Z7J?UP-s>TL8|Er(vq z;-5nl$gb~8cwfT4jCm)McX?I1cR88~zG(Z*nHjh<IBLgl!0?q^jPG5C4k#r9H|L5o@{5QJDMJb6N`%XV$nXsAw*`}Y)D(N5kZ_pS1 zYw0J(kC6I*bY24b>Eu)i>Hq)v^rJ_l(pW*SoHvF2srZwuo*c19X*skf1kZTc*LY#R z@|bU?h2R!!@H|Wo8I{;Rlzp_=H)4~utl9nqTdBN6?1FRi^%vVid0{6`19dOSouNkF zxii${J9mZ}5a-S?ej=yz%&XTLn=YNLydBAuMS6^+sI|ke1@@fu9 zIJBf$oI3`fG@N4GItG>pt@7X$Tb`u9r9MFSS>V-C^T3t zoQcVrvDn58=ylDMXX-S_#{fc*Kchct&xobDe#+6`>#$h)P&;!Urdrg%uhbxcu@IkW zjK>8``3RU20Hy?4QoESJQ2TQ*D=jl|157&%vhXa>RvBc?$a0w|xwOp@&K3O@**|3d zE8Dr)w@79{nZ_!|b)IbdKMFld++C5lQ|;NsS|&cM>`wcboz!@>I%{>)(%o_n-%?J7 zL^fXbPw9Jes`i?l9Ur%@+40Zsw{o;p2^OPfeD^H>IlKI-Gqr3ntk4=(+YiQOm@0LC zk@;^j{A-ab@uZ01@{3|yn}(#Pv0kxX@l|{Kqr_Qwo=}6>{~qyGPs%VsrU|e%kdJhS zZQi@Fa+~HXA{_-3kxl}&;H)mf>*KQFe=CHERmMz$-$(K8S$w)oeub1ca7nXFf#=B- zc)m=57cT0ze4s!j7AHNGHh#zHj#=G2kF@Y%;~T?R6nB0mPtXf*7*+swdcQ{mpzE z;b;4O5q>6D66G-08)(gZ&pu*Tp9wEYg2q43&|iT=f1aN7SK!c}r}9*xP;1$D4e^P| zhB4j9QhIRcuU+X+4vysnC3CJ>E9aUC@f8~4i}Gve+g1K^D#)D;G90@#p*4LiRzp;+ zlFDi*T;|W#Yh?a(rC0is*o9&*a_9q+HyF`n!*4tr(z_*w^n#n9y`05oM9Gf*pNz$9+^1Kct7PY*pFYu> z)(4%*>10OIGUHdxLyWD6l3IZwH3GZ+TPxo%bmo|&DV-GnVNpUbT|>;TW@Uy^B>qtb(FU9p%a*>+{`HHLw@8j|p_ z_ws*cmtV!jr)<8fl$$$=ZL~?welGnN^g%zjXo5*OBE8ElEBG*F}xq>v_zHe*&@n4cUWE-UCeqWnQmg4$;b zH@CzbNJUEt#x}^cP%tNxaCS`($~-g^u@BopUJtgo&BjVhSJV~(#8swKbgMtjJ1UHb=LnH`nrOyjiIl}e3&XjCN+I^o|K5XVpfz+0yl+qzUj<& zPOO{0I{%uCy0*EgtMftnTj(j4SN%j5Z_LIx>^-%_Gm6twf3RNoRSXH83<(to30(;Z z4Pw$9xMVG?REWrMBuW^l3oeHz>C|nwl3R-;^rqRBqnYzpZI1OGX4?lvb`YE7rxu#S zN@ijF_V32zB#%yHeP-E)PDAjI{9&e7r6sF;HPGLMy)ZR zTJ=>LPsF?o;bcocoP)@Pkp_|vtr2nB*hv#e3kay_;ee;Dz3k!JJZ&FoX@4twYKmvn zduw(adT?jpHmAR|zKpRNl#Vsh&$O{d{FyZt8AIHqj;|8`Bhd~McG?>Ff>x6SSQ~h| zRo`N96{ygUGy9R|Z!kc4tEkH>f{|-PUA{cRwesh{9pFFE8xxk*-K;5#vu!hDi?h3( zcvD6;IJC=<6DFh#%d?vEb`e>nj_i@qczKj~uRDHjG+rKkjdfNH;p(!7ThjJ$LXq;% zvV9Xh{&eZLH9Nm;IH)=eI~-K+PqS|*4IcJ?`rzxe%>%zPpf0q$pH-*lf-+5Yuj+0M zykGL??xW_wIgPSY4 zrJfT!Em?qLU&L&~F3ujL#XYT@0?Lle*3G^x@3Z!?>L-zgZ&xGaY~7?^0({wTRSeDE zZ&h0{_$b`S0yqaWa251Wta+wo=a6P7W&2lY_S)tgE#j4QsHI?*MxP|nPza?v(SOkB zTQ&L=iCz;E{X>l|)#x)Nx;Z8~pwUU|p3!^VV~M8Gr-0m##1kq91Y!I75$6Y61$kBh zR#fxZL7VMicgreRGP>MqdzJsU`RCA7|2CSk1sDU@BFrMcuJuybybxKz$xfatHc0`J zOA6sbRXZgSR`0coi!aLmC%a$3g8wA}mH$@7<6s(#{g_ohk_Td07Wg;Dq>y_=I5>4P z6`U=#ONUf>XcwaDI*iIrUH91`X(eAC`UB2r95TLphmZlCOW#(^pp`aif#<8dtRge` zRyqh=?@&%SL${Z_X}NrjaV^+hmt&W3Zfqj%2cl#( zuv?YGO)u0jBsPVe@8iN)h$2fa;uQ~0J(SQIo0dt>rFxd~B6uScXj|tY(+^`a!QASk z`Dx3?`Wj;vR1JCM!F+@@+aJ2TAsb<8c@>i2q<+ZX4S5OBr5ry@L1B z3$p@QzVt3xXPFyPT#G2q67OE8N#3!OkcB}%Qon7A`BtRgRyyB|{3D6yYQ!UUM9ln% zGKsb7OxC>dEezC31nc1V2xE;ek*tqbR6n0$f0j-n?JfE- zwxiGwAJ&7Mfp5E$AD68;XIihY!e zUQUzG3+%p*5X?aNdNW_ggu*ij`b055vP*GMO!!lLb1?=pKa2cq^yfAO*^K8=2uBw# zR@uWxJbO0la_^PMvJ@E-K}=F5%yzMAro%>$F4_vAzx9KF$hpYQM7IFZTGGb!p5_)S(C02mZ~(uoz9y zMqL`qU799QMvv@P{cPbT2tcwOXx4L1TB(ukJqd0x3*dQKWq$~Gu3Vp?m3^eY|1A5% z%_FaD=hN3cZQy94r)_)L!;?L2uarG}v!`vB{_f;Dk~4L&&1!(-9#;APmAgnrn!8B2 zf9sCOJtXVpa<6CUrpR3+>wV2TJ>AW|K^sJH1GD+w08&PAzE=<~_#49>5RLRzm;GU} z=gQWMXwhqSPB<81Bd_j3N_6Hkp||ud@1>);lz|eOr2o;czijtDI`-BpWbY3}JIkhT zw5D12eFair3PdfuXFCFayOE&%yUMLjS;Gh^e2QX48Oo|PBg=cqX#2KbGyjS%xks+` zlqmXT69HEh!8}r=M2_aeHn}E2>hld+PjyV2TfxYV(Y)$SsoaY#heEpKAsTr=b_VHH zFQtE;e*^}r;ueb86Xsza({ikkq$L}*)F*FL(?It$^yi(q4J4eVH^uCQuDW z;`6-C*H}#>Wx?fd+aegErmb5>?j89N?EVr~V6>$I*;i6ku*@_mopgg%`P(HuX@@%W z>k~fHSiP5Jd|eKNiFV+Q<;^uatiVOfU#)r93cU4rj#d8}F$Y%V1U5hJvFhb^+XJgS zfi;hNt@=0g#T$4UF^qLh)8pyfx2xaM1CKo3&#M2LzVr(`@OXw*KVM%m0zp#ClE#}> zhx*`Kpk(~Yuxv4)n{uSS~XF>@bC#eabOKiyGpjN( zLQ*{{;Xtci?wb${{ZT?%wNgUbvW8GmB0#$6QQPx0kq6dL+VlEAaa`2R7s`{9{1kGo z`sRTW+w_4VckrO##mI}SCS%`ONxJGI1Ix(hG{rkRj4|&SaqpRN?*roAStiEhXN4H^ zJ~-}uNZdOszL@l^@M7Ls(M6Wy(I=PvMKAJMBfXedzV}a;DHC2TlAfzrmB;MQus%tH zFz=+Z<%y!W_qmbx-sG=GlF48HnoR!sUK05$u-ul>oBoUBtxJ;0TMpn3|1kJk>U?JY z69n$L#s1JYdHgR7Inz5ThevxS`sCCn{PI86|1pxe_OIOX($%?J$9uH@J71HbYu=^* zJJ&|vrT;ru$ov0={T~*I%IS}2|A)nW(cS-HJ)EL1>oHgVt7IPS_C7y3?MWQ`zgym(6)SH)4;dtpw~e6P7ZLm4l*E{S?S6@@cO7}D7x}t`?nxnE57N%eVCodf zScD}6BNdjA|5OV9zgeD^MF-MP4e|!&L;u6%XCe8pTofLapVt(nV=%*=$Ib00T|t|> ztYv3$Vli?6rtkjjmU{oDmJi-;Mcvfe-R!@anTC6+Gc2yj6Nu^?I@1Np{tv2vuiNOHhKgAmt^Am}gBQbO0Vjh?{ zhHWLeJG%xi|3B9NdZDokii`(Y0n4UuZK%eo-6IV+B2ZVzO=K1GsIQ_*iq7Wx=h+_a zz@G1IdCAY!Vjpi`-}3JEww717|GlMU`%zSQucN*@!v8;MgsAYgvNQiM&x$v+1{d)x z>bnEsxD-^qYStYO(+JaV&D_wP`fv@?A(U|Q|c>+NnZll8e(f11>5*BH_oL+%>c0=^|# zYlyqr)rHZXb9zjAE7DU|{a0zELkyW|G(G$g#u`nvp>}P0bK<5q{d-#4-gK*O6W%|; zuHZgZnlIfUHp^pP8P{yxw+NCtS1@7S^BdAYkhL9itos`IZ1lxPD>6iz&eiKKRMPl@ z+Et#~kY~|bg{uHDVH&IMbeia3))|n|9v1m(Y1mZ$d1>H!Iaeci&=-8UNN?vI^NKX) zURgiZcI8w}!*m35B)Weoy&5(nzMQuFMtkHvm>;8l2G#DC3eYi2~V^OUoM3& z51&inGby7iH_(dF2 zn_wS$;p))w+#BjaBmXH@5(n3D&s|#JJ<^SZd#tMoKSWwI0S5Q3<&o;M{zLpVM%n zyc|M^te0c)ZF)FtyhV|(99%otx8twY5{N%G{yO77oxg@x!XlD&x>bLw4E})b8 zWcToeiO0fVzapc{o^1_--AB`U;jiIz;{T`e*YK^dAM_pnkH7x6^4BP1i;P$7DPu(0 zljZUaF?q`>dup*~)FynR&R~jT?+0Pl8zWw!e~n|WVtgz!zcaj@cZapAA4dhA3elo5 z@Yb>ef7DuWzi`EgogeiE9yf8=Jw}=|rN15blcZ9s?61VjtGc+wVM6wYFlQu~XG|ii z`j?Gl7bsLVlKr0-%hE6Mw5>1u{zaZq7yz<6#Tmg|`YvbhzUkS+y)klQ{X)5VeY)qY z{aj8zW#p)}G-wmsLk-%>^=x#RO(M310+V#nx}of;@{xK;z5Pw@xi5Qa#>i1EGg^Q7tvdx*CVjy*Dcc_N98JR8eTZQ!FwwEH#icx_jxdK%@!+!WQt zSuCm}xGo*gZQOXuN)B!ow8sjpvOJ>W6UnP|I4Dlo;po5(H?=DSiLMHd+PynJadYh0 zFUI$v_MP2pc1O`c(owD+vg+^d!Cc>gc?$fYc7Y^;_-gd?HhtBd> zWLMiwuK+2~1tBt(A?JKpotJ&EOWf^%oJjRb+3KTV2Uai%DnJH!it#IjL=aBH}CM8}ZeZ8RxGbw2Jt8 zSt9W@QNj~q#n&^b1Bo$4evQwjQfV7q+e3a$nDR*>f~fo|2g1b4ub)PKZ4mQ`1M5x? zbVQ}s2T5rUNzo4l_leKbjw^j!|6F@$bk()BU8Ae6F1Jp=F7X6xEc2#vo<-gae#D$r z)jk`_tuA&%w{Bkges?!ZoBF?@A@c1GQ*JEoHyGd8Qw1|4y-E7_Re$ke2 zaUqvAkC&>AjckcyD!YfnRBMpgCI3(gs8EYo*$3>=^21!HAy%@fQ*N?TcK#|l4Vku8 z_y5zANT=WdeH%>2{nO?%loGcqlxHyT$S43Y? zQoAb0y6=HFNGjNfU9dgfuB>xQvR*kY3fmh}QuTFiDEnHCB)yWEFU#OB z>p1X-snLSNRF)inKjU=aRP>j+lp9>7B*$M(@A#8ywr7Pu8I&4-2zm*}@2lkPfxm$U ze*=#Lf7G+!Fh9xhH=7z$;V*S5BV45<$6s3S_>*e34-Bsa-cCLY-5Z#rSyV74nycg{*!994+<}$tW@}8s3*kVbn^DV zA7exCH~2X42kTpKn97pl?=85i19>w3m%5Y(U8VGbzfXF_pH#DbaQJe{N`*hBnuPcp zPTn5)gC`39h8zd}n0gl+rn2Pt`x&!)D*UA`QeMNbhE9OE2l_kgDY1Ehsf2m8^rY^mzA?OW%?Y-hps@Xm) zyoj<=;g6+WLi|l9Zx8$-+YA1(j{|=!eHR?2vgG)C3++TI{G~4CL02igjNeeN_>*e3 zXNNDRtW@|z)sPT>!^ztNf2=J9f7Wr}4?WL+8+zc70ueg@fSeT%55B^~a!!yh z{UcxSwWaTgX3kf*&(|Er)Ar@g6QcS*Uv0?Ckpx~}{Q+O&!fvbXFVGY1=*Ryx)LS9{ zH}E5l-W6XJ0Kk8nyi|_nftQNw1R8KraRXoc@AJQ(4}@(Jw(!4}Z|kJhD0CZn-Xk>- zZZs9hNdRs9`w!abT1E3n#;slA*&gdfWxU-Nr7g6sp+vJ*29Ob-Urj(z6j zF|U*aH*KvvL$3`F_U<5s2i~e}cj!NvGv_B$$(*Wgc70p+`0By*1ET)6R&I1bQQmaP`oJml zN4gw>W!32o9ZF6^^Xfu@ont~!YP9pvFDKG}UnctRi$Sr_D*Er_TJQ0yUoal_X1~ff z@W<=Br3`f5=SOtj^qZ=Tqq^@eb`DMx-8UF%?xFuyojW7Ahwl4vQBB0?zM<<_{>Zis zCyReMBkyEi;}>!ko<`NNUxs$JxlmGjrxe*(I(RR5#pds%Y#E192Lo6h4N1sfKW*zL zQs}m6VYEEuiq|-8vN+)khN`W#?s-!62CH&3&8Wm_zH6uHTw(AZ(VtgPl(0N4E_7+_ zq3o(3g|8*GY$`hP5}?&)P|>B*YSzqiKba;r%4Kr zCKY&w0nfKd-4h-}jfC(>>KJ$u>wbYpF<$J(IAWF!#mR0c82yHtr01e(4(N(|RZi!M z9{r(-r2Rvyejfe{>3X&wQ8QutC3Q^uk&dN5;&iVm`U8R6hU7ZCl|AVzn&xv6NRrT( zCKY&w&>x+ww|m0F(jy@}k~#*S#JbQF@UQ^0;i=Kpej@scrkQ1@NlIUuRNxs5JfBVo z&)_8RNa`4PSZ_-I#OWqez=LpL!}Fx-9TLG4P4itlO;UI?slYQx#y_ch_7AFwg#9C_ zW8mRk;EB@}rho?pjSWw`>H!kL6HT+yPLmWKO)BsVl<`mMp75ZmN(hgnj)8~%5_sZt zcPZeZbQ_+Wr0_)3{9V`X!U>X$A5ALoWC72wN!=43Ku!pcq>h1yu`2My>C#fbL+{)0 zc>pTqLU<%~3_OW-KPlj0cC+ExgglYBf1+uAW2Z?9k0up(`UB6`NZk`2 zrrd<^Na`4P66<18z=O15!=q=9C#1J%8u5P?n3KYzNd=yM!1H5L_k;%#C?PzOItHG^ zx{(y{APw5^jJDg_lOIIW3{3@(CKY(nf#(GVKu>rOQ4_)=sbkLq<^19 zgj>rn5G{lHL-bj{;1by*ixHZt>o@Kr#mRL~NWC%a14J&MW3XYj@VaTH1v4=`vwdv5 zZuwt67jErdXZtyey*=-EG@B2R_oBG>x%Rv9XZ3TIuhtXz9d&XRsd;%yUN|>KS`he? zyl_^GzWmwcpG@A5+vMDt3QqTZw{70smN(npC$TyViTx*w{%yRAV^K3l=26x-USHu= zjtRPgXFVn;{%9clGh6hOo_KgEv)YbS_;`={AM1Ha{;4~pvd95IC!=_B5E$(UQAH)i zzd6r17XRVT(J8U?6~%v8j;)N~8~lgaP@;U(AK*Xyq3a#|hu`A;zX$(?F8rU{3;sLD zOZ#nncaBkdjreG@ z0LY^uYVRB>+}K!`RRiu&`rxm4NL_LzDTnH6TC097v6_357%-FFs{cXclh`fmrzazy zGR&vI!+_c@?so`#b;OD#Hs-TaIrf#)T3zSaaB3^(+2|p%=I~ZB*2tMUoBS`C19m7; zM$PB#lE-3sY)+JYMZOq2<4t;gA#yQ4Q4jS3BU5l8ZZ1lwd_)YVdiyFrbj;{4rmlT<~Q054yhe zB{LIvT|I)phC53SuU0v-e_=lE0N)gT<1$DWetB^7nw__Q+#Gn<9A#IDckH?~(8G!> zbls>AB?mKQJD1Zed_#N%9J_VN+$7y7hqJXx^mA@k0*RLM;gk5gKF-nm-h~YkpU^Ao zR}z!ZE9<+e>%3<&JVQJQxMIN@b%I5a?e>Fh@-2P8hGA*la~jEI&_$eKKjJfU(0_(A z?BcB(Co*aM02fEc8+HoMjyLRh1@(NsI6kpc_D=+5MYZAfQqVSHg5r%|bs;vE%7Q-A zBQYrcyU|jLXL2;1b`f_6-7{O{VV65cG>J2w)W0ATT!=sHsRsUdf7rnH#Jk`w$8V|~ zhToejtZ&?^g_jfso1+e~;+-#TDrzhp)Sjrp(eZ%;t)y%$9oRl@L)8`oaWbN@>i3Ec zTvXlnG|+uc3q#t%c-L7!#S|xUC~PbJ1busGet65J$HoR-$zEZsv2 zps9Mjyve}nxG&s?VSNwx1>mU_x7(|sGWB2hS~9=eM~j}pL^n;E@70#yrC{DEV8*-N z1}F_!b4m+0EId)J+!LXjAAfdDc(Cy`5dJ9s3>A()gBB9!WqYpjfx`nAzFhQ-c;%XU zmk4_AT4L85m*p@$Pt^mZ>IdVh77U1wLshRnx_>rgo3ZekqG!ZaS1Y}9zEpbW{GOGn zUqqd$H6szgpQ2WrD@2}g`|lRQgwF7FZ}6WQb>iI-af6VShu`KFSC=EN%2PU@lqZ-^ z{?^-ka+VO$Vt^3>iIRD&2-oDyE^VA~$pwjNl?Sb5>sKe4UbefZm(D*)f5tGszqol@ z=lv3&j8z1FsZ2L0@m|=&6mOjWdCK`k4q`io`Q;R)E7$zePZwnWr{myGA|HS*xh4>9=O8SPfX7Q&~IR{9%mVBNDI;cOE=3_FaJ zr}NFnC`jl*r%RTwCY4t&yRKL2`>{~uKESr-`JUdcJ=7NStLz70sR{M z+Y?Y0iJS@OIX*}6lWca8In|kpX7_a}I@_L#eoA%rG}EKKzk$`oOhRwcXaB-SXAb%j z&q?PX5fa6P?)GF;&|}9i1yxa4vMFe{;mFK)DdwN^l}|ALZ0_^?(@pm~^Usgt=AR1c zO+EiSXK44B=AWRcHR1eo$jl^ z8@0WO{ilCp;u88#+v}hI==>z>AKSlJbQb^l&eh-k!TKVR5$=TI6L?tPn83rjND+hX zBi_$ij!!Ka)H)eVi?ldA(Tt|&>&pD)^v9lfWlW@H`AW8J6vt{d>+jKz=sR%ycH8o16(KSz>1`^ci~sTA6$TS{*a_?4X03iKnr* ziDx=JW!57(RprV2w_TIZm67$2?fU%t#e;3g)Pq~jFiJM}459V3OQxQ0I7^>+2WoNU zzR*z5z=f|QoO%*vNjKH%(&r~}Q_l~9pYe1`)wi9sPkafwj*;{J;K9k}o`iYitahMH zIQN`tW?I|+GG_gAChMPfWc~BD;{?7dcJ;G^*~dt-&}I$Pr22fh2B&T;i$R%Kth#YL zl?1oMdx=jMD><}+$Koaaay~`XcIH_3Y~$%a>@R-H8e=op!(J|8vBz#z51YC3sCr6c z>8JKi9`}NE_f*~{bKOtf)$$O0A=a8sRh91jEp(kJ{K+@uIWYc%C);*6MV<#Dc|vxc z&IgSAYILQ2^Dm!HUfC~<0$0fw1@0^I5PU&Oi&(R<9+TbpIL~t#q%M7}?0s7IJw{SD zR~J7(QaopC(n&5xA#psT#_Ol2-LG?G@v_@jGDYX501JjIb$K6h)PgB*gsZ&7_*Z{+!GCt2@Gq6)-@b?%V#k||@naUJHizH`{SwBrhlGN;XieGA7UY9)4!{{#P|<6 z_#fOS{7dEd_b;M`RP>LDWkUQI5 z2K>{G6z2zwr&^o-c}j)<)a8AMMXpW%uJRJ&Kh44az&_z$D(8HGMbwar{*&25PbY6I z{ww)n=wBY<@SoUP`WW!fI81T=!1L5$)4%#rxI7^f$Umven;$9fL05T6@Q?YJ^8c(p z;a@7}e1cD)%2fDIX21O^d1LYa5nl}b%R?Og=@OZL;gBOn!~qW@&} z@vo9Mj{f;#=wBY<@Xrh;_>Vg|;TZ7GIwZyU3FE2OrhlGN;XieGAF^h#>EBge68hh3 z<3FQM_?OB#Uttk7q{2T-wZ!rdd1LWk$rnTa@(_oAL?Xd|+{p{afPa?0Db8Pbo;9{j z|2(C_f9mq)N6LH9RbCSOx7+yd-zWS_<($v(30j#{_(xTd5dW`|Hx~aN@x{=;JjCH2 z30m+UcXGoq;2%9yit`)BQ>{(^Jf*^a>heBBXK2&EtGp!m583$d*C+f-<(%)Zh#FGi zKbgP8bn?dHzmhM8{^cPK|EwSd|8XZj90UGQE~hyE;d!dC>7S=m_)k?{pwg7}6IWT` zFNr$StG&7U%c?;d`N?tGA;B!#UKBw-yDxW{&NiKsQ9skHzmIf@({nbAbvYd&}*zW{&@zH z2g%VR?gSIJh`7$RW`Dq{ozGiI;S1J1xA73~xKH`X{q=K99j^U?Gx(wn{**jK8l3Eu zS|HH1>`hKfWBvD=m|;wdH}DkkKKfkZ)Mb&>Ke1DH{-V68hyRi7&tE$3b*pwH3B|F$ zCY;Zc+WQE+$@KFc5>dAc-t+mQ@D|F03tqUJ@P;_ud_s7c4Qzk0ha#) zMpIWtQvb$Ioe*Bl5V}<19YaEK{I3b0%~J$krmlqZ(P$FI!aIR43hy|1aKX!LD)7eX z!V|)a^kKs*=QEi8eP36@iQtWm^Yv zykFpp!doH_{~vkp9v?-K^$*V^83;)1fCSM688s*oRHEQAB+(3Hpa*&Yl?YzANPvJh z5M}_C%Y>N$+O~yt1<_TPUEN)E*Ok==jI7`!T#_JSh{{zZph!1^5Y3>Y z!OQ!8et$f_S3hK`t4>v&bL!NoQ@2wldSMO`z0KC1Tcj7(u0d}YGd50d6GAP^a{~IA zIP^9Hx|qFlIfj*7)I~i6fn^XYnAA2w$|Gsy_j7Y z^w!2_+ig)?%k(Co{~Cw>oXyQ~$FrsW%Q6`B-VZ=>G~m}qArz+o?=y&D^vV=4(F=S@p3nDOaQ=9N+6;OvD1`Ul{s^^9Zvwh5 zKG$w%K(|OQ%V5y^2V@~f27Y}pLUDR8v|hIiK*{v}6)}w7=P6*K7xGB-p6}V+d2Ir}skZKVbkR(_4oaMsE!TO!UJ1AbOjv{j^9gMtg(aLKMR3 zeH@{d=}ka)h(k*Tbc^(|3hGcpVB8JhshXN*gVO|ow z&DL64q!-qlLGK6@!s&eyp_b`QKwlMyzR!SekzSU;p!Wb~#pGzgukS)APVa@*H|hbD zOz&};R@31rodPC$F?}FAlc;0v7O^(Mxw&7`>;FWTqGMPNKKjT0)ETG6xFq#x~2#Y5g~{#%aaH z*%mxA4B?PL1JeBd$a(W4Ry?D3rj4951Y_*w=lf<3z+7?GuXECn3MUOXJ6!i$=l880kB)6Z!+PkbloF-fY)F%s%RO4SAu1_;Z4U>iOHDbXBU>!oZaD0Ca{^56e4I-x3 zS%k$=2r?D`y5acNxA-kZLrsHAMDHH_JBSE+iP-xiE*yW3gs%`Wu00|&{M(CDIp5&7 z0*Mmpa1)-M|L z(NECXRpLHtDQkZzGsOpwm{OM?z!py!6xP#1N;Ow#QfyJ~@Bf!@albzg|<+$O9W7o#_P~!0M zNPu5Kq*hSER*D=v9dBnAqD9DsXK6P|3{zqyiabE@2dF8LHtj~l=oEvt-1`M@L*@XJ z{0;=C(2|$1_xJc$3f}IWMr7~PU;&@-gy|Hf?MC7@L_;LJd+As_ zQoL2fN))jhkCx#(ve6Cipw5Jt2*ESN6yg19(R$kNun56_LaKAa2D7vK-){BwigZk@ zzu|tnwf{(~mE>b&tJGkf?^{uIM&sw(-PVoZy_8ceId;$O5aa&K`m?BpmL5eps;9R$ z$(!5=UWECz|7o2;()cJ*h^Ck}g{VA0WQ2((#QX$Oh=Jpw1-y-nAjE=!)24t?faPF6 z5{c-G92VUxa)yI>8weB(Va4vM;_%+hF;KJ%nj9s9ZMv{~Gek_-XVvxE!!1 z_-V-R>v|sDTU&MlDdzL5)HmaoYLLY4%c!~7`0>>lnwezwJ%Ay$0Yexi7Dm+rkOixQ&Z z#Q>`hpgkn{O+R9Upr7ziLWGAa7iu*`SN`C(b~S=4SujL^_?p7MSh^e-9>;gSDNg^qsKt9f%4FGd;61i6MU=OAhe`4=?Xu?%M<1}_8k{5__NZQ)^#Rnr zh6`6ipqMbRjzQ34m75DR|Xned;e(AB*{IMYz#Tu%rH!A@k*D52FTNU@MJ5;9keLc}W2Kz73stEjlDCyG7O zSThKre-V&6X}2WT-s`;2ImS8GIc^NZ!%y(4vnkMM6&8sAf=Tb)*evrjmO106i`rV& z{Sok@+PCVRHux#H98p^LML(gtLVprt#1)6B0T@pVG$r-TFt8OBh(l-zg;f3{Gb%*Y z75){C1bLO@<1bXMRyf0j-h8%Sw^A+eGF2Tp|_BLXhGJ3slb~)^(I`|DAnG2m73LzLj*WH zMmQ)Ohk+c2V*F@-3H}5Io1xbOdMH6Z$)U42bQgl2323ALD;eMILibl~$GTZ8r*@Pb z=mBKBHi$+cVl!kUo^(EB_kO3yBaw1)bBgyX#kLO7r)pnE$zks`$V_LuPugBZY`Slg z|4_Q9X8jrEA*GsjAp{rPzo@n@m&4CaLZu`^ar@wkreeBQEg!m5R6V=Jz|kI1C0z_v zGDZ{SPeir2Ui)gQ6{7FEO;milL3OS1pZoZS~!|(P|ZO|*Reoy)^S-$}R5lRdCuoo**k{_;i%RfQ#YvuM9O<8mf$|w&{ z{*isR!47_WfPke`JMi^6m=BwnRy%Qh$ho;6*{zY9j0Y4 znlqG|HuN%}C?A(Aa@ing+Rq~W?>~K`@GM@mtP!nn_S_aWmoDbOG4}ztI>mi-D)I8_6(7q%Tr-uz{K-=Tq8!3Vj4bB=w25hcn|ZTdjTquWyp&^5Im@UN zy_A=-{k&4%HA?C1mgi4-J>3+T4m2#ifg~RWD;DLJx}psbix>?}2YohO3cHEs4PGQ_O5}xKDkhVPNw<{qVjxOJei!l=r|5@aj+AO&{23k1Y|j|| ztqJ`dsANJ@t|Apl0|iV8Ww>1gad6jF3M;S}u+f?L+7u&iaBSyUiL(qlp zt>OAkE^ZKIPos7M#I-FG-%p$X^ioX&Oce;Um(yg=Tw<(a-fA8B`GWYIK@fGOXk?$oKME3^HgVsGn>@ z{Y={JmVeN`n#lB@7yYOj{iv4dzxzdbCgdQGMC^XvdsuN0Nc5gNP?q*jo^NDQJ}2^Z z7Ud_X&v>1nX?H?{7K(vQcB6BQ*{EHF{vuY9sLO0~l;(dvSq^6>Unv-(_2-2ys3a1@~9(9Ef#qt=^CZQV;$rgs*VIhoqaR47uE z=>Y1dEoGm1rP?jKz zd@Y-&+KUQ}pC* z)SIUcX~MFOu;^YiP@u^sgf3wQV=-BSd|VVl(_2z|g%k{%KchW*N~ML&AQ@G38K`w3)2iM5Wr*?|0XMt>}gRA zv3$)9cd`VV^(TXY;FTnwhA}EBDl36=m>GtY%VZcjM?3Gvh$iKH@3w9WG}^rHzs^kV zEdS$R#T|GMBs;2qz-oeC4QQ>%PF`Gr#x%@2!=A$zNRP6aYeFAr|2#zmr|NsS{(FIV zYPQkHqU&Z&Se5HvV^(D)ezcDtZ^@$j-U8@3RKMDHDTgj5=)ar?y%c3{Bk0!d^-K20+oQL3k=QgqGBN0<_R*(-18N&ZG#7 z2Bl%~;iQu(qIHl?G)WaT0@qAK&Np97mQ2e*w;Tp;tSmR?dum!Ipf>YHQEhjrQ)%~< z)6p(Ccb^d4Nqs{;#p{GE#8EK7XY2q@EQyy>Rgu8ymiWrph3c=2)$hsKiUMz9^$$_o zwRI_7f-Xmwr>K4yQef5hiTZhngU`TwB^5Bws;rV+ymE+|wj7+XxE-g3g;pqTN%mAr zIH{ucfg(8^%{s6GO`>?J$ue7tawWOitv-e+))7~3R6Mm?l;325u(t2*{`r#jT5EvyxlpAFVfu zj*Z@NN}n>PqfgNL9r5pWDRtg!puuge(jP!$B!rj5DmQErEXh$R1lyCogn6r(ejvn7k|(Y7$Vc4rZnUNfK47u^BjPc* z@07Q(h%PbqJcIoGKd^z*Fp{!9b_l7YJS*ea@d6$zg zP-D2LF#shj+zl*-s%bZZ#D$_dI;|+r?wtZ~bj${~{3RwtSYg@dk`H<0O>RitaJ5~& zqt0z@)oEb>Sgg~pLUE9D z{WN3^Y+k3gM?`FXLH3N2F|3-+&u_ze8qLq`z%(&G*Q;oLenVli`T6C@W1gQMwA1`N z4YBe0`LSyu)R>>crj{(g4S{G9uLt2mev?SAl&nK0c6pB6m9-37e@pH^^|ISU>L zXVq^0c0coU3AZ})$a9m@QOFJFb-dV{F1#2%wL4I4CI6Ldyg=ee!H(3C7u%aB(|RLI zFJ$S37fc_|(#M~lZusk(HO!ujmTwWI!1LQI^898-*!L8rjHi_G%~GCmmZQgG(tM75 z`ij=zo-7v|8=rI7d=t2}s`gYb>`{plZJyN*&qgnIEZ~k##_2xtS_HO=c-_gyR z_nq5xa8ll$O$S|hKhd8vz$3c(r(L_?|Mq2sV@^c-ARKcdItbyId$)VQwK_$*l(-^w zig5*wEr{tYG>4Hwrj)_&CAslIy#xjpIdX05JKdwggofnZ}#Uli#ZWZ zG=*~=prV@wxKGuc+96ja+=sr7o zYfG)dV4+EJeI+cdo~6|{OB-VL4H|F7c!2@=vS1qJOoH6x%$pwdom zaK6l`ybz2K>;NncPA7NZ7@wELoZUpBvy!~50iS4Y9dq=_TRU_1NbY0};!p-Y>s&*? zdyVhKbu?|v!NB_$WCIjXK20bm!(q~bLPd4%rOV*9rPDHf9-zsq%{?)%HY~$w^bezO z@@kWhJnP`H1?2+_B{$7CN^S;tiBUML8*is2x*dg2UZ%8GE_PJ=+K_RShM0}Qimf zUP$-({y*zS{RY$+*N?~kf2bc5qwuc|G5a#vbK@IOr3ufC*Vf`4hO01yz@=N69z`p9 zz0AH`3Jurs>4X7DTRzZ_%zRvGC-wKV-IN-Wf1b{|>(hl7OlRuz>6jq}^=kIsXrMHV zUW+hr@+~p>Zj7bV(-F%3IvN!3)ke_qec-6!9yzqppJodb8 z6H@xx^QtF2)7O4&4YJseT|D8yxAtRWtrHG>VL$dr%7m|c_U^M%znt(&vAz2V_zxU* z<+WRYU~SnE#`MMHVDH*$yiIZCwH1i3=8fSSPF@?Y-HO*KuDo`mDSj0BY`k{cgtzay z^4iUb_=_K$4s@%~Q#MsTdTMKtuyzM%sYy0ZX$%2cs`O<2`NN3W zePgWQO@bqYdXnnb{7im|S~JIuaU9N3Wd@^X84%5|G(n7N=C_fzyV2rCBcpMtk(;%y z5E=#=Vxq+jR&v>h@h#@BaY;+GA>Gi%LtECy!qP?Pu_n)rCXM_oQYJ5BRHZ$~q>DFE zyA>vyas!p=WVPJZlXg~ zOExdr5vac{aPT(&0YL~lG1m<=WeN*zP%Chf3*UgCG|<#rSl9_V*?$Ny8Lbi12$+rp z(+HS*38oP+R{;k0^TiH92o1vdW;#?c48O{-G{^8XA#{@w*Pq3uJBFtVp(~8IYgk-6 z$MAMSsI3v#0l(Pz8=i?jcj2(rEF4^$wKev3H`EgJk`VeFC*=vsPEa`G)iP8kZ0ZtJ zZ-T<{u9l(b{uDO43+a@eelwWMNWLdVGP}J(Bx~3eW+dO%h8q>8^(cdWl*H)AZ=8N3 zu0M-2=r`i7VQ~iiMqCH{63)LsKTsg3U!WgcQik<*4DT(3-awdedz;1eaSZQ+a|uS= z%Pg*+V|YIyw9trqlEvjahUW{RM~t{x_>FU_4LB9IFB!`QLll^ljAg$-9Arr$vX|w$&M(?|fI`oPg zn5=-_Ni2X~@gnY4nCa#6aSd>%Hn@T4G5SXbBaZG~DZ{vY5Pk&Lip6pHIAX++Zxv-& zrek=f5ZY5C_qPc1pYKmkp?+nU-7(xQgy=pN)w+b0mhBjxErcF2;=C-bhhumTAvD^E8_(i; zIfnNVLPbWLgx_E0*A~>;GQV)x6lYnPzQRd<_;SS~S26kagL_CH2f?2O^LN%*1RCj> z?d~m63db?3GWAO^{IU7+h2iPG5X$?R9!>s7P@)|r+I2c9x$NiaAODNx zy=*`be_GZMpYs}fqsHF)aMakW|083og#LONf=&8s5V#uCU;SB_>#rMFnCq{r5GKi> z)?=Kf)=T2Ng=06IuwxL^dPxws8W0z5y@xpdg=|b~L4N^*3(A)r)Fj%E={~~vg76J6 zAby^r8+eIE`@!a<_PbztbV}~$%lp!RxG;yh@EVQwBTF5N0H*Pp>c>Bl;TG+SE~p{S zgKpT{|Bd6kYN zqy48mGFE=)yg?Z5KQJPb%I}gl2&4Y1Mp9Dw_PjwD_GF$ED?dALP_}OlD)b|sm4A^k zxJTZg9=;O1pP&rxl{cuDZ*(j@sl48KgL?Z4lHONT2KUJu)W_E&22cDzdHwPR_48fA z%FE9ild-yhrRs40p z8mV|AVI)tz%Z|;sQCI}uRT)n&Ya;4|MO`__*uu?+W{oQZ$VLL8bEYVE@_20Em5xX3 zHoRjI`kV?bYqFrFgvCE_5UCSwFpcblo%p$z`e|dq&v0QCo|_gcH&NEN`}ECU>^rgf zqkTW2+>fz2GJP{P?c(x<>1Dbgny%o{0Z#jO!5Z{~l1=YUIY^$M27>}$)7mqh(HIM)F+@havkk-=z0;q3y zDU6b90LCME^7w^wX|Q z;DeKL#oGhvQl~IbkS+~_QN*}>7FW;W>Ww%tRu$ezuu~i2$xcNiR!XgsSV?I}l3IsZ zTG+(q*nSUBuV?A?&C~Iag0QFx!bgj!iT9ruTnnT+fj`&_q*kRTJ!@hAV^N3i3di&Z zCj2vpVu?Y(zfPO=SvxkL-c^Y=@n~-C2QKSv zukHW(UA(HF>Hqr80J%W_*Dqz+3t?Ls`XRah>!;9TNR|D+>HqpVme~)jK?{EW`}=>O zDSfu1911>4dM>>A%rmtU4$OaJTBivo9=@uZK(BMtTiU76FKjqnjNZ_0QB1+GRppB( zobcHbpty&GmC!|xz#L%7cUe)Cz1wiBQI-%o0p>BIW6=#Ups^_vPD}z&N*w4b0I~d& zQzx7lgQQeSs)11%TFLVl3XAu|fTR^?8SuyzxNS7|IWiwsU}NA%`!RuS*3gq&cM=~9=D5hg#_L@2p zgs=lt%syZ^x4Sh)6$lu4E6BoDltJBnw4T^9elxw-i6Hve!iI~tvJUrNza{&31}B%KSWv)HIkySq|O^r7#RwS!;JS~ z6Yp7cJ&Uf7N5}R56;1Eaa1L2boBe2X>-_6a2=iOvA5FVyF`*)-+ULM`(TcP9D?NAC z6#wW(U#2l0r^iaBVLCBBQPU@zr|4wn^vqU5rn#!^KqP&(C}b9`Xb>``=Ne4a*)}c< zRHmXxHEkJ8q6Q+VRYR=eM5RgPV@j0C7EughgpkI&R_3e;tb;CwBVdb=iK$H{TS}=G zGVkgwWR9an6I>eK2X6|yHae$4$Kx8K4 zRA!u0306_pLl<0y1B?yO1IsucXq9i)U|Kc7jzH#JAa*PfIEpAHQul$E`^@-Hq4guo zr3*zz)9nA|GzBWhR~$)UI+9*P{uSH})&UNp(VQMTL){)non6ca%?C5pk?pMj-^$oU zz8}OkwQ$El^fu2Fzu}JcB-n$>ZMaOCds6R)gRaf&{aCu1mGWGB+$GS@{0aflWRF;am)#Ghin!hFDG9sr zrWjpRDlXgG9UO6vbOsKj-YLoZoN}F5aVEuyn>=OvSW?B2Zc@%}accNspyE=qJLK&3 zlx%ZWobBeBTGK5QxgG91#FBZL?uye_i#d(1oC8kzb7#fTZmygiF11&eBKbp4NsXuE zGiSw5$WljH0Gon)=2yFNj<|9T7Ug{6DXF7-=Q(?ECLZ~mV~VU>#gaPASD?BIMYZD= zvBcF`%&EkWU3{=EUqm{-5W;EZ6tSeZ#~3lEHa1@;{uH}$l`VD`>m=R9qKjB_K!jK_ zJX0ze+zuCmmVkI%Y80o|{PT@BuERYmer+A6*sGP+V#Vnz`5k_&pU9zlz+%lr4!zF; zb~!}uBIMeN#%?(kYMNXASl*Fahx-aS74DLO9p&v6-*wAD8{i0cRpH61RNwE#@|^pY zR{q%IzNO;2@+-iH@+)la_l&3%QFyBg1oJx;71b$jyuThu!TPL>OR+D}v32KrgGi~U zW0v7CbA3B2j<>hF1I&3+T&bvp9^+Tmuu*L-!n zg&$&J3ky$V;Y=1L$Rb6h59cV*Ky`D5au%Xi;?@X-m4nI&M|@F9BebR1Q=r?543!tK z(knUsM%4N;5FiF%9|p!*;7y_$hid8Sud;kP@_N)>X}Musa$6pj87IJh${@)spTRXf z6`+_Z6@g1tTk7#KF>vcpA#^>;!F3tjQwkikiaQ$Vjyha&1hXjK5fOsz5OB+%qphjA zBst>z0GV;4?_2#};25S*&6swmC8phnEHbljl+bt<#tm~Y*37^uES_#`C?PYGiAXu5 zAs&>?!3RicU|%jjf{h4*0=mot^mjCgRw%38QX4H$ZlKd6jTK0oN(_Oit_;T}WT1hwIgwRR!Fp)W^Xf$2O;rxaixkAx7 zTNEZtMKTjEnMoR#1)0g0!q_8+h*e66*aP<-EKDLLGgc`fVh`>TvUrjlnXyV??2-8) zV})}^=DboEgP@E2`e-&+vM6haqoI>?x+p61RkOSjR{W9-G_n;tT}h1qRRV2l^nN+h>DcPM9VNe9nHzQnn(~Eb z!{U{2UV-~l(7+W?Gm^DY@V`Vw89h>VEpy9NqP!ovin=KJChBCJmAa@OpxEJK{T~hB zcM9!O>6_T(mIwC6_rOewAOh*bu8%~PSUZ+qwCk1W1#Ne|BA&vH{Ivd}jP;hL+u%#1 z71TaXeAlOnbVKslN_{(C(-hX$6)7;36RBnNxUnv$NkvLXd7{wrLQSyo=v*U`vXS+Sd90bnMf^w^jqx7YRYp64vFH zsCEEJ@_G_?s5H_xU=k@AXf{$MlZBnEi$<<8=q*zIM%~0&wuy>#mYuZ^U!4K(AW$O% z)wFDve6UDn5L~l4PwjQdyPWHg4DSVByG!2D|s3F*&z zfj2D_eN0tc%MyjI1_by_Jc4p1&T!>ALUBFI6)Zw4@$X+FiWJYQ+O2eXl@w}{vZo7N z9J6G6LXSiGW0zu~0igGQvo4_B6YEQ6z4;^dPjTCmX`Lo$8fYCMSWl6Ty+Uvo0+@lY zaSX?k_Icz((4px1q?n;%i#Wxgj&wY9D`__&=OyaVLQaVI7`Yb`UJhaQpi7Y3r{Yyb z$~@dw4P6J5ZlUn;C5>FL^;K{$xUfkZ{z-8So!b~HBMA)=|&~laBbCKR8&$+TR&7; zE+(0KeU~$w6<C$PJgBdmD#L<`=-tnz~|GBbsb#m`r_jR&;6|O&u!ErQp7% zSfO33K6_I~YSW5H_nf_IQHmh01hD^ht3?@=4kaZ7X&*Gulp+MbLI579E*9P$l_^fG zBN(!AOGb5;hxdTyN2nu3$b)0fBFMYcvRFACp%HV#D9N|Zm0JVDrw9eLlhm#~rM8y; z!sLHL-tH3Kt@K!{2Mg~?1J8NnYRqb=8jqT7b*cl}z$z!CuU5Mh(r3k2DLK0*LzSim zzO+0TzFaKX=>&85w8mL+x?3O&Dfk3)sP4F=T9or)(Sv6u$|p8_v~Alq6t$%&=L{=t zW@>1kT9j%P#4j-^t(a_} zeznVH@Z6HKQLe?uh}MEwNes%VR0kkjE4(}SVj2t={RAL)$&Z*^(Ii0aGD}qZyo}qW zPT}1?GmCPz6sc*I&YWjPV?v>m1Q zN2fK<$GPtTH=GXKW%&Id7bEZ{()IDh&>kV&o&O7pajvHuXYQOIBkUyFsDb8fsNUfw4>|H4k326OrJ}5D9(7WtyUK;ii3F;pRC^w!qvQfA`TF3Z_Lj&h zm?DusF*@D2p03;`e=n@PRIUxw?+<9s{L5zN!iqE6W9pSw@N9(ocQfA+S zwLU{m{7w13@39DEb-s6K0R%~I64pYi?4WZ6|W7>>Fy2Y10qA%@Z7_c5wPCc8EJ|Z`)^@YB?r${M&KNP-Oc={Ao^scLR9W59PRi%e58#*Hdr=4kzT469$PVx2! z!}*Tm!r+}72xe*>jo4P`pW-?eORG0o{$P8HQ+TUVE^6But2dvDR(KLh%Vzvb^7(@u z0V-adq%nycV;Y|lga_f36Et`}%_Yw?2605wWO-gKXeVBPAke7xy%@w1FF-CM?=Co` zao+8`2e!`AGE}A5&Ia0BY=pFIyAo*XG=Zab1m}GRs%(3ZO#k5`-5p3t42uD+cc8J& z?8D4c*r&O52_}PLSDn!YljUMUV{r-jRw>>dL*wHPDVDp(hQjk-=9fG4=OA#UTEAkj z+gL9zX0GUBzAghtdrQi-@Ez30@Ezm>2hTbE`Psc-cDt>coWk0)?EE1@P=b&M8)^eb zdIy@?2#enY?qbQ8l6w7_g=EB}-AmHztSYkMl&!_G4(FQ@>#Icv*4~>6uP-nY6!5F8 zi3aT1Zpl&aEy5ZTvK>fs2Igj@TYWZoZu6375*7(hT_ZCxg|(UFUidi_=Wn`E2$G2U zn|cYsClMfif*iAVCn3}W12bAWrhCxVOiut=vdmBI4IJ#9?-7Fc0S$P03lr*~qAyVa z3ZN>h3e#z}!yYl(0w@rb)$Sskf`L?^{R^QB>p5DXsc1T&<$VAvWkC8MF(9j>9jmhD z2r>?#@>(&u)IIoM;|DQsLs@%h*-hwcCI+HTi1miFY%UAJqJN_mbvoaHe6V!KTV27g zgJvFKr2b5D*F-krCNwL}RTE!9;{)f5WLw>%ilrxxS=QT5&uwKf5C zJN~`}Yh`3cN1f0Do)S0$0>AKM5jqShfZy0Ow<={_>`{xdGCkHK5-@>(zpQq^b_ZVm za8?aqe1{*7a;+6m2{dBF?+Vi4LZcjZ#ejAgS0@J=FA^5FMl8i#%wy7o#Xr&j>A5Ez14U|7 z0QNQNg?F5lkwqYpt^Bj1UjWT0`jJG;O5jUl7luJ3w9W73yWNkevTvx8LFFh}(fLG0 z-%M6$!5(~R-j1XdZ$S(OzpF&0;2p%4Y6VY{<1^b|@j?ITS2O8Bdl__jFu4P3aunM? z0RoI|pWxR(+K-^9WW0_a!dw3kVv)eI1F}Y{l#D@04%K+u1P;UeC;b7ZbOm$9q8Yfm zw$4?yZUCWPr~SKx98_gpN(Ey^0d*ey1o6UJ8Yn}n5JcPFM0ugAen;kkykC?tvR_|F zQc|o@j$tSYG<`Xb(?lE8RW=KXr2k5_Au>po_iM*rJ%|2FGJ_&)uL4@5`$&qdJt2(h zTA>_29B~HO8BzGB9YZ+p*D7v19Hn|fgbp&3Lr~qz1ijFb-(Lug0~19W!O6)Py&bBv zW9>{2#_S!SRFH(=IOD5SdwmHIR0~FCyT-(Sa!`y2ONHOAbzs&P#9HrKn(Drrr(8F3Rc;cBSilpa-k6v za%_JZ2XSXUPZ|EP{>l6`%R8Oeiy_ygK4f5Sx!iJ;`QTFqbyQc<-e9Xo{KRUqwhARw#_i%Q2z zr(Be6hpsN9p~78K)EbUCM`rhSBf1B=WV*@Tjg7a4e&Aa#@*%i)%*sK9zT22nRc#EK zO|f-D3^vs}VRgv%&nMbjgif|k=_kkb(%4M!aTk95Vg%?dw>E^^!$bNF0=c(B=y z^)-XjV>U{(5sJ*Y`?N<{cAp2XkJxxv<=Tb}7JW9P3r0YYKG1({&)n0-QZ>*{U)1}c zhpPtmpf7zoAdUGz?mm4!zOdGKkj+Ej$?$}>fJT;PrC-MKpxlDn!6q$#?@v*^Q=%Ni zJ1Bl4{N?FyLP4Q_4aJxf!zb20OlUEyd-X90WoVzbn}G-G;LtQ(6(p2u86ldV?el5S zppUYcGeN|lf1uaghY0%5y#5j^-#LoSA!D1B{m{5YO837bM>9vjpTUPYt@KerRkkvt zNchp@?INsg4M(0(`_~sKw%Hhpu)hcQaRAPTdA5k{c=UY$=CL-TKU5+mu18|-J`hr> z4P(d_EMS;B>7&Z_NURF88$np?xDqCoZ6ZWX-D(;1?+2sdPZ;8Jsi>}@^*7Rqf9uVU zyuS$zN-R_Rz7MEU?I$>h2k(@W@GF(*7LSCss=Ev#d)Tg1S3IOnkD zH54NRHz2Mm^gjsG^hdP9A0gX`23oDrdbAV{s%WoNl+|kb!pVkIzL!RO?R}sE*Ra-; zPnBxg6X+PS?QS5=I?#BkUNO=!kK!dLtUR!#oQky`7Z%<@T}EEsi0=)A$2x|Ly1s!b zOyVtMg(9`F#i<6AlJT|==RIf@3Bt@0os@|F1RiIsr$ke6K<{xxXw3mlA!wzUoos9Jw#OSi=uZDZ zU3fNuaJ2}*Jd8-#d?Z*jcz0w)7cfK#ovj@r3?9Ic#{!gAmxn%~WSv2ABHA_71bY#H z`{-NV!;sYe&0q?4F_V7=&Wb^lCkLRvhEVrvVHSALU}=9e%*%|k)aZR1Gb#m!~3=M zh$fi}4Mn`Zf|cb*PK$p%X$tW9T1W&IwJ|xA$r5rhMoHERd^kyvxinbO&-ZIL-kiz? z^eMnse*uL}UIs6=TrZKJ#2O?fqP*YN+(ELydb`14x9qa$?a{LE&#S1^HvWnVu5X6ZMJO?OM574TSl_a@Huf;d=foL@Qq*~v z$nw4?FDFUWmO7YPO%p=v*_3Etx|laG6E8DxIX3z!+wcYv`Tqd zoe^9zz(%`()pClk@D{Wfv@7*F2=&)-;v!qFBQo6jH^O>_dN%CD6szOv)F8z-9h{#3| z8fB~4ct??oHoc^)CO40W+1C`@QEho@n=R9jR?Jjob54I+z?I535}fnvLnzSWUn zp6ab`MaK|xb`|;$r+eGsi3Y6Hr;Co`-b^0O+%ljMnUDJ#JYLE+3S zPHf_Xbq2P~n8h&V=G6iM5Mk#@aRan~*i%vF~0jAXoYsEA%kiXsH+bOJ_Llu=$ zip)B!VdJSsi480UWxq8Xh01B$*-B|o-X&L}h%FWv;empsNCpQkqYe`&koi|5G-#ZBA)?jKI>4f#0OXIauy?EAaXBt@N zI~2rqVLH?-X9WQ7lt0xs;R{EKN3n6$I0;tp01{&c6M743<%8plo4^+bYecw}U|DRp z5-cCVw-lvA%1PkvMjED4LTCiBd;md0iGB4KOqhGIY$o^eLTEqPK59n0SK3(CJw=kB z_Wf)K7RJhe?4{%K=}k7`WX?(KCLW6LQ~ z9c|TyBaC&8FbbDZxTMap;<#&`_79wkP_m{zz?4CdUL}A$K95iAj6cZKxhKrlG_cwD zvB;c)Z_1TY)O;WmEi4Repw@T_+@ZD6136>HH@eV>O z3s#s@$E?Oy9xiW$t=LKUSi%z(Hw$aqwQ{TNs)efiwZ->^Tj{b94@dd7 zNY*n18q0<(zK^ir@ER7yS79GewmVOKj?J3FG{yD4r{rtrlF=FXN_TE4YzMO?>p`F( z$_4k~2ivzF4J9`8-Ohfx|9J`{_9>*%f1vxZL~wE}XkHia{SFI2{Y(o2uzyhq{TJDZ z^@aY;$Ld7;V`Y%Gz1;F=v{1N>+P{Z{Eo^$qB9$rCzQ<4$X*=k*Y+#5t)P&$hmXxuL zP<1$jrSH(UIu_l!;~rcV5T35U04J9JcmB6CX1C8M5_2$@Jdz@wI*6Z+7=MI?Imm&I zug2aj{FMYX?m5GRx2D|&NwWoGf{_4LcIgfFMA{08)4||+BA7y zn0`s6c0`;u_%QW}zSD;Q=e({tgsM`|fqt;lP5&d3 zXO>{IuN!v!qzXfCAH>Jp3aaK!mfce>d&1hDZ%`W?+M~vEZW7~ye;^fgEMq`1^$q*Y z(A|GB)2n3EP$mri!9s8jv~~GCbln%!go^Dl6d~J|P9_R$y{SB=oM%x4W!cR8nu;Ru z3{RK#Lhw(B5!Mcb%{&j2lB|aqMjb10Y)4r81fhPM^3qCuaadc_gDumCNx}D`cB4!s zsiHipHxByr04cBXs{h6^RmP4Y@V!0$C=%TWZHQbNTc1-!EDQ9X-wpSnGVy8&X-+&s zw5f+lMydaa$_%^(<-_sm{=Rx<`t+p64xOA|=frDEAi@%NLbL_xsiJ1t_K;bL;dsp&6-Maw?CNh+zzP8zM~+3l17LA&ZF8tkaW- zs~Jb|leTQ_zw(S{=dNBUtN52Cj2;Ywl6ih(|tO13H z4@$vMDg}FgcvB&)&0;0vLm@?KdycZ&kQL)g8}zA}ZAC}^4&mw7U_F9fvpnNy6p1wY z)1LV!^#7763u=NI$&Ddpc+LQ-j;Po*k<}U2TA@A&@W=EGhyBpzJy9iL-Gw2K`}KSg zrtkj{2(FU_Xt2Tq0(&A@diR4t7qBW>|Dp*d9v%o{Y>N8lq*=VWVG!LFLm>1OLtq#~ zAmbl{7{^|tAE2XuO=JYL6m~+y;ccBRsL!LQnBa(NQR~deb)2ut92_kig*F_KE`(+h zQ~=_I+?eO;*a72o%xU02ncnUCFaV(e{#ruqhh6d$e6bhftab&^0tVHz#2(c$f8^8o zS3xaaSg2-`a7rj;8o@RlC7_`D*-ysf^aK5a$sH}QVLK)C2BV{38d6exk78`p)Pqz( z*I^9S_Lht2-mmS-rg3*}s#SZKr59Yx)1RYsoL9t`J{DzBe^D~ZSgwp2_|e}4*4R|r zr(aIeqIB&FP|~FOf7m)lM%Prl1;bw3R{Op@fSC>J@PqRyY-#sg3X+eIK&pKg@Z>ir zxf4&`Ps!i&)cq`JDO$+U;n%)(IR7h0F2RA{1@p+gW@l zAb60Z-T=z5PLoOJccQhl8I&8wUeB`fuPKiW5nY-80Q0%da?DbrbUX{mTOQ>egnpHatj*pS6^xIHbOs~hwqc%yL zdt>=;qx>5Z`CA+LqfA@-k->z|`T4=4(~>$E7miq~8-mrN!72PF(>GsKoSiXWxAqqE zoS8Fk6yCinlbx$YtTHHFl%00m1X;{>H)pVoEV;q_zNK<|y5bPkM|;Co+a2SIf|IbG zdK_2Qw4Ntv`b8UJhzP;daqXTz1f}9s9DKrKJ!D=zHY|`?)9TmLY}`{17)_tVc!Tj1 zcM(twE#dQGu$zuSIJQsjy&_g;pkIo&J!U_{kkPc?%K4gma-!Us`P57##rLa?yH$}i zwCfM$hr!MG`RN8apxr8>C-tZDYZ(YT_ebqh{z&q-SaB@X+gno8&S7azX+^!IQ!XA? zXZN@uVf0wim9@SftRG`J0Y^Z$`1asA@LaeP-^lvETW;6@A{La>{;1p0;1&io#4wtD z4b4EWFBVtQ35@&{2^u^HoA`&KSryPxq?C9)=cyWAKf{-1Dpa5q^Q4)$Tv1g~kNSvI z3`EstcPb2@@<04W_-B42{2zZK{D%fSoqwNgyc_#-^e+4L<&!>%oxUqOx*8c{@|FA` z;#tf&hv&DkOe|7*XqRGAj8+zc-w`}X>_+XKu7`1`YzrN*dksNRK8UBjhN#}&7Y$L1 zdS5Q$l4RKdB0$X9Dmp%G>AB2LeUa|`(Ny-RYv?sr`uA9W?1$zO=_Mq*!ip2PzrUWg z=~(Q#TgCx}4L6P$ey4l%xVuETx2Vo(i$leeY74U#$l~3b{@p4 zjU)l$%85mCPh73K4Pt;djm^(8qK_vZ+T-Jk)7X;gnw!@K?{}jRL(T(I8r}n?({!cU zC~R^z8-Ky$k$2*nE*@z!tL@!tfOcU6jTZ}x4uIUUGYbALc&zLUc?H8eBhYSvGpSgW zWr3@3PXO^aWgWU15jfGf>H{n!TC^WAF0uhV1?v_o$+%QJtLVBf&Hc(wTP*3=8Xj1= z&SDvl;SJZihvDpFJX)WO9Gs4_Gl+$aU4)pjGfgZ!g@x&nGa-1Eh2Ns^)3hQ`79p7l zoEai4rggr+nL)zh(G0b$L%+k;PD$De(5|;e>tlL9`3S|rG@yzn!sPulKN&kl&;K|T ztSjWCU4){w%Nc1nUkK0*j1aY8hl6csA45}OzZ@C@BbD(y;GYOJ8n2k?F)`2|OYpx3 z((Ae*0SsGB!|X{6Vr*$^H@-wXSEY@_=@1cjvmn;t%h1YtlfTq z9?LAoYgq@q?S!=*;D|2Q-_+LID{$s|VQ~rSz*VP9chG?l?)fhB|Bjo0OHQZ;8|4Yq2l;(VyqxAFC^7Rv~4 zzS*8(ne}e~%gZC%Zh^iN-w<;7ulPl!=J|TuZXiogZv}vIu*9OjLLrEVMSmV4NP{0G zu?rpLI6rUEai9Tr7!lMTqYyfTMW05Y5=xy!d6Gr*Fl*2P?f0Wlwp7XcV76{KzmkQNH&Oa`Fm%f4EouEzOoG4*gtqb!hzYY0 zp}Q6#?Z9QoyOLe#fRx>QuOVfxyR_&!OCi#zpG%dx3}8YF4AJLN5oE#XG!-jHO%@%; zp_`KwLA(jAcTtme6R$3h@}YE#o=YK%9ieMk2+Y^JBBZ5L?^>z1r|{>bZCC0xgqyK# zadI8+e4A>~uHZR$VJ=m!p=;Hee3oi|!*nEWAC*r8YB2+3 z7IwxOk9*-HjT8o+v8`lw5ay&X#S|Pq!hwpxFdOkt8H^Pd{8I+Q9K^q>@NvkX>2N

Gs|>3U%L1F71@T~m`zdvSk} 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�A08F@f{w1Q?HPqSs73kiTmtwDD}@3uu)f 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≫1T?bHgJ6BY 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

m13n?VXaIUS+)l&te*+fi|x(I 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

$2zd|IvML(_f!YxZhe3 zKBsWJ8ibD%j)lh*W)3U|;XZ5N-`{S=>%Gzc>@jeYhyiaG9_zolp9*dlz9%IRj)l+n z=UDi-KgUKmo&>^uX8)UG;XEpVaQy=KGs0!z`x^tov2bqx9RE3YxazOl{;S6p?jwxw z7{>75eGS6-6-K!KGEn02|K$6!{Wp*E=X;!w>x9>Y3oATV>v^n%t^naV6$rQed4B)9 zj}sn4__~Gr3y;5wU5N^_5QOJ;2AHzKYhJi4T)!BE^ZaLo`%eeqYY-mq&uj0`v2cH3 zgkKs6kMZxnX5oAbxBc4@#f1(eyrukaiR1Er1viWQzy9&U_weVOI0zRo!u#;T$PD|t zvBKgaMo(Y*ulh#{E6JFbUpxNQ!l=oA^-Whx;arN;dK>i-Dw9xJD>T;KtgrCvbv<91 zFTxAZKmUZ^9^sSRSkn|V7!6+RGh;bEiNnbYnbqC#0K3Eo-wwtNf6ZTyi6h~;5a!S2 zKNa2z`13sd+uNmZLHOQ;w~B=MbN~K0?h6vam6$)*YvEV}{+thin6EC~65qRJ*`0L!_I$>>Hm|^4bYZ(yMW`#M!nmOa=eptnC$dWx$ieD4Je>Z>5 z$DjNCd7b>Z{O5J@=kmW^7yr8A&-47RD@3+_O5VP3+Un85`~3fYLkb;E_b7cwR;7y{vv)#wCgk9r)ht z&y)P0Kg_>w5#6MAXyJ}W+3v@lmhTwTyGOoxmCIT;lLyk}2OhhvOBO4yXshebmF`XJ zD-93qU2`yU@xkCpm!8Ti-t5^i&v2%7O~eiFT?HF5ybLCLw5nc@8;~^U(bTnd4^$MD z$H=Ft{g|7zc+Shn)cHgT*!_wZIJ+ps=hhxu0 z>y0C%7G|$XJ~!QCYu4dZ!vQB}7#&;{FXoXN(sN+f;Dfx%|&-_A35M z>&wjpTc#^+-gf_3^vD5&Z_l4P_{q}B6}tz`ZPt9aNFiv!@;k?_jtn0gQG2rUh-1fW zS7qDh>C1$A`YsfIJs?{?LAg9wHc#_P$BbU(7piox%gNu`n-zKK{D+3sUw^s$7&~i4 zbEce@yv&aSUM}r3w@pF%R-N{UK-R8BxKzBvP*x=VzHxtjajgS2iCTj69?^u81teqyZdUDhJ7m6$y z{H590aF1fAs#99V^KN;JxK?wr+rh*e5o`O-{AB#BWTCEWcxe~=6yu50N9P}J){I!J zUfNN8s(rA0y`v$}t5fFH;N4pL z;@&3AU;S9kV|~Cc{fFjtX?rftjTxnRD67J};B%Q

)fX{dO2{2+v*BT4JKM&vEas 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 + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/partial_vm_edit_graphics.xml b/app/src/main/res/layout/partial_vm_edit_graphics.xml index 7fdcb5f2d..1f66ed78d 100644 --- a/app/src/main/res/layout/partial_vm_edit_graphics.xml +++ b/app/src/main/res/layout/partial_vm_edit_graphics.xml @@ -69,6 +69,13 @@ android:icon="@drawable/ic_connection" android:text="@string/create_vm_display_backend" /> + + +

+ + + + + + + + diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 1ba620f12..a8e61f2a0 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -132,6 +132,7 @@ 找不到任何控制台 文字控制台 %1$s VNC 控制台 + 原生显示 外部显示器的 VNC 日志 @@ -204,6 +205,7 @@ 垂直DPI VNC 服务器 启用 VNC + 启用原生显示 (virtio-gpu) 主机地址 端口 密码认证 @@ -693,6 +695,12 @@ 无法连接到 VNC 服务器 全屏 + 原生显示器 + 正在连接… + 正在等待虚拟机画面… + 已连接 - %1$dx%2$d + 原生显示启动失败 + 软键盘 额外按键 全屏 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 67e7b2cd5..edf8d8390 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -135,6 +135,7 @@ Text Console %1$s VNC Console VNC on external Display + Native Display Logs View Logs @@ -206,6 +207,7 @@ DPI Vertical VNC Server Enable VNC + Enable native display (virtio-gpu) Host Port Password Auth @@ -732,6 +734,12 @@ Failed to connect to VNC server Fullscreen + Native Display + Connecting… + Waiting for VM display… + Connected - %1$dx%2$d + Failed to start native display + Soft Keyboard Extra Keys Fullscreen From 8198c1ce7244b0dee0938c916f4753334c91cbc6 Mon Sep 17 00:00:00 2001 From: HuJK Date: Fri, 26 Jun 2026 00:13:08 +0800 Subject: [PATCH 043/182] (feat) configurable swiotib --- .../daemon/vm/backend/QemuBackendInstance.java | 3 ++- .../droidvm/ui/vm/edit/basic/VMEditBasicTab.java | 5 +++++ app/src/main/res/layout/partial_vm_edit_basic.xml | 12 ++++++++++++ app/src/main/res/values-zh-rCN/strings.xml | 1 + app/src/main/res/values-zh-rTW/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 6 files changed, 22 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/QemuBackendInstance.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/QemuBackendInstance.java index ffc7f38f4..6860a19e2 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/QemuBackendInstance.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/QemuBackendInstance.java @@ -177,8 +177,9 @@ private List buildCommand() { switch (protectedVm) { case PROTECTED_PROTECTED: case PROTECTED_WITHOUT_FIRMWARE: + long swiotlbMb = Math.max(item.optLong("swiotlb_mb", 64), 1); args.add("-object"); - args.add("arm-confidential-guest,id=prot0,swiotlb-size=64M"); + args.add(fmt("arm-confidential-guest,id=prot0,swiotlb-size=%dM", swiotlbMb)); break; } var boot = BootPlan.of(config); diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/basic/VMEditBasicTab.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/basic/VMEditBasicTab.java index ead57cc5a..607daa45c 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/basic/VMEditBasicTab.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/basic/VMEditBasicTab.java @@ -31,6 +31,7 @@ public final class VMEditBasicTab extends VMEditBaseTab { private TextInputRowWidget inputName; private TextInputRowWidget inputMemory; private TextInputRowWidget inputCpu; + private TextInputRowWidget inputSwiotlb; private SwitchRowWidget swBalloon; private SwitchRowWidget swPmu; private SwitchRowWidget swRng; @@ -53,6 +54,7 @@ public void initView() { inputName = view.findViewById(R.id.input_name); inputMemory = view.findViewById(R.id.input_memory); inputCpu = view.findViewById(R.id.input_cpu); + inputSwiotlb = view.findViewById(R.id.input_swiotlb); swBalloon = view.findViewById(R.id.sw_balloon); swPmu = view.findViewById(R.id.sw_pmu); swRng = view.findViewById(R.id.sw_rng); @@ -71,6 +73,7 @@ public void initView() { public void initValue() { inputMemory.setValue(512, SizeUnit.MB); inputCpu.setValue(1); + inputSwiotlb.setValue(64, SizeUnit.MB); chooseProtectedVm.configure(ProtectedVM.class, PROTECTED_WITHOUT_FIRMWARE); chooseBackend.configure(VMBackend.class, VMBackend.DEFAULT); chooseHypervisor.configure(VMHypervisor.class, VMHypervisor.DEFAULT); @@ -82,6 +85,7 @@ public void loadConfig(@NonNull VMConfig config) { inputName.setText(config.getName()); inputMemory.setValue(item.optLong("memory_mb", 512), SizeUnit.MB); inputCpu.setValue(item.optLong("cpu_count", 1)); + inputSwiotlb.setValue(item.optLong("swiotlb_mb", 64), SizeUnit.MB); swBalloon.setChecked(item.optBoolean("balloon", false)); swPmu.setChecked(item.optBoolean("pmu", false)); swRng.setChecked(item.optBoolean("rng", false)); @@ -175,6 +179,7 @@ public void saveConfig(@NonNull VMConfig config) { config.setName(inputName.getText()); item.set("memory_mb", inputMemory.getValue(SizeUnit.MB)); item.set("cpu_count", inputCpu.getValue()); + item.set("swiotlb_mb", inputSwiotlb.getValue(SizeUnit.MB)); item.set("balloon", swBalloon.isChecked()); item.set("pmu", swPmu.isChecked()); item.set("rng", swRng.isChecked()); diff --git a/app/src/main/res/layout/partial_vm_edit_basic.xml b/app/src/main/res/layout/partial_vm_edit_basic.xml index 139ba7162..6dc7aef43 100644 --- a/app/src/main/res/layout/partial_vm_edit_basic.xml +++ b/app/src/main/res/layout/partial_vm_edit_basic.xml @@ -76,6 +76,18 @@ android:icon="@drawable/ic_shield" android:text="@string/create_vm_protected_vm" /> + + SMT (同步多线程) USB (通用串行总线) 保护模式 + SWIOTLB 大小(保护模式) 沙箱 大页内存 预分配大页 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 7e536f825..8a6eddb06 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -176,6 +176,7 @@ SMT (同步多執行緒) USB (通用序列匯流排) 保護模式 + SWIOTLB 大小(保護模式) 沙箱 大頁記憶體 預分配大頁 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index da1818c09..5009adcb0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -178,6 +178,7 @@ SMT (Simultaneous MultiThread) USB (Universal Serial Bus) Protected VM + SWIOTLB Size (Protected VM) Sandbox Hugepages Prepare Lend mTHP From 87ef459793d62d6f5c7ccf27a3500082664331e5 Mon Sep 17 00:00:00 2001 From: HuJK Date: Fri, 26 Jun 2026 05:55:53 +0800 Subject: [PATCH 044/182] fix(type disk path no longer jump to head) --- .../edit/storage/disk/VMDiskEditAdapter.java | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/storage/disk/VMDiskEditAdapter.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/storage/disk/VMDiskEditAdapter.java index 3b9c22c3e..f39dd4102 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/storage/disk/VMDiskEditAdapter.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/storage/disk/VMDiskEditAdapter.java @@ -28,6 +28,7 @@ public final class VMDiskEditAdapter extends CardItemAdapter= items.size()) return; + var disk = items.get(position); + disk.set("path", path); + if (path.toLowerCase().endsWith(".iso")) { + disk.set("readonly", true); + disk.set("bus", DiskBus.CDROM); + updatingViews = true; + try { + holder.switchReadonly.setChecked(true); + holder.btnBus.setSelectedItem(DiskBus.CDROM); + } finally { + updatingViews = false; + } + } + } + @NonNull @Override protected VMDiskEditViewHolder createViewHolderInstance(@NonNull View view) { @@ -79,6 +100,7 @@ public void onBindViewHolder(@NonNull VMDiskEditViewHolder holder, int position) holder.switchReadonly.setChecked(disk.optBoolean("readonly", false)); holder.btnBus.configure(DiskBus.class, Enums.optEnum(disk, "bus", DiskBus.VIRTIO)); holder.btnBus.setOnValueChangedListener((oldVal, newVal) -> { + if (updatingViews) return; int pos = holder.getBindingAdapterPosition(); if (pos == RecyclerView.NO_POSITION) return; var item = items.get(pos); @@ -95,11 +117,12 @@ public void onBindViewHolder(@NonNull VMDiskEditViewHolder holder, int position) public void afterTextChanged(Editable s) { int pos = holder.getBindingAdapterPosition(); if (pos != RecyclerView.NO_POSITION) - setPathAt(pos, s.toString()); + onPathTyped(holder, pos, s.toString()); } }; holder.etPath.addTextChangedListener(holder.pathWatcher); holder.switchReadonly.setOnCheckedChangeListener((btn, checked) -> { + if (updatingViews) return; int pos = holder.getBindingAdapterPosition(); readonlyChanged = true; if (pos != RecyclerView.NO_POSITION) From 19e5c25dc3107a0bb727be72caa7df8c68e19f35 Mon Sep 17 00:00:00 2001 From: lateautumn233 Date: Thu, 25 Jun 2026 22:27:32 +0800 Subject: [PATCH 045/182] feat(mthp): add mode selection for lend mTHP allocation - Create LendMthpMode enum (DISABLED/SINGLE/CHUNKED) with backward-compat migration - Migrate daemon CLI args from --prepare-lend-mthp boolean to --prepare-lend-mthp-mode - Switch UI from binary toggle to three-way chooser in VMEditBasicTab - Update AgentVM temporary VM to use LendMthpMode.DISABLED - Add localized labels for all three lend mTHP modes --- .../vm/backend/CrosvmBackendInstance.java | 15 ++++++- .../droidvm/lib/store/vm/LendMthpMode.java | 41 +++++++++++++++++++ .../droidvm/ui/agent/base/AgentVM.java | 3 +- .../ui/vm/edit/basic/VMEditBasicTab.java | 11 +++-- .../main/res/layout/partial_vm_edit_basic.xml | 5 +-- app/src/main/res/values-zh-rCN/strings.xml | 3 ++ app/src/main/res/values/strings.xml | 3 ++ 7 files changed, 71 insertions(+), 10 deletions(-) create mode 100644 app/src/main/java/cn/classfun/droidvm/lib/store/vm/LendMthpMode.java diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackendInstance.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackendInstance.java index 2f9c01eb6..210a6db30 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackendInstance.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackendInstance.java @@ -38,6 +38,7 @@ import cn.classfun.droidvm.lib.store.vm.DisplayBackend; import cn.classfun.droidvm.lib.store.vm.GpuApi; import cn.classfun.droidvm.lib.store.vm.GpuBackend; +import cn.classfun.droidvm.lib.store.vm.LendMthpMode; import cn.classfun.droidvm.lib.store.vm.NativeDisplay; import cn.classfun.droidvm.lib.store.vm.ProtectedVM; import cn.classfun.droidvm.lib.store.vm.SharedDirCache; @@ -186,8 +187,18 @@ private List buildCommand() { args.add("--disable-sandbox"); if (item.optBoolean("hugepages", true)) args.add("--hugepages"); - if (item.optBoolean("prepare_lend_mthp", true)) - args.add("--prepare-lend-mthp"); + switch (LendMthpMode.fromItem(item)) { + case SINGLE: + args.add("--prepare-lend-mthp-mode"); + args.add("single"); + break; + case CHUNKED: + args.add("--prepare-lend-mthp-mode"); + args.add("chunked"); + break; + default: + break; + } var boot = BootPlan.of(config); if (!boot.initrd.isEmpty()) { args.add("--initrd"); diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/LendMthpMode.java b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/LendMthpMode.java new file mode 100644 index 000000000..d45676c48 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/LendMthpMode.java @@ -0,0 +1,41 @@ +package cn.classfun.droidvm.lib.store.vm; + +import static cn.classfun.droidvm.lib.store.enums.Enums.optEnum; + +import androidx.annotation.NonNull; +import androidx.annotation.StringRes; + +import cn.classfun.droidvm.R; +import cn.classfun.droidvm.lib.store.base.DataItem; +import cn.classfun.droidvm.lib.store.enums.StringEnum; + +public enum LendMthpMode implements StringEnum { + DISABLED(R.string.create_vm_prepare_lend_mthp_disabled), + SINGLE(R.string.create_vm_prepare_lend_mthp_single), + CHUNKED(R.string.create_vm_prepare_lend_mthp_chunked); + + public static final String KEY = "prepare_lend_mthp"; + public static final LendMthpMode DEFAULT = CHUNKED; + + private final @StringRes int stringId; + + LendMthpMode(@StringRes int stringId) { + this.stringId = stringId; + } + + @Override + @StringRes + public int getStringId() { + return stringId; + } + + // Resolves the mode from config, migrating the legacy boolean value + // (true -> chunked, false -> disabled) written by older versions. + @NonNull + public static LendMthpMode fromItem(@NonNull DataItem item) { + var raw = item.opt(KEY, null); + if (raw != null && raw.is(DataItem.Type.BOOLEAN)) + return raw.asBoolean() ? CHUNKED : DISABLED; + return optEnum(item, KEY, DEFAULT); + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/ui/agent/base/AgentVM.java b/app/src/main/java/cn/classfun/droidvm/ui/agent/base/AgentVM.java index b7c4ef863..0b4ca11e5 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/agent/base/AgentVM.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/agent/base/AgentVM.java @@ -29,6 +29,7 @@ import cn.classfun.droidvm.lib.store.disk.DiskConfig; import cn.classfun.droidvm.lib.store.disk.DiskStore; import cn.classfun.droidvm.lib.store.vm.BootConfig; +import cn.classfun.droidvm.lib.store.vm.LendMthpMode; import cn.classfun.droidvm.lib.store.vm.SharedDirType; import cn.classfun.droidvm.lib.store.vm.VMConfig; import cn.classfun.droidvm.lib.utils.FileUtils; @@ -108,7 +109,7 @@ public VMConfig buildVM() { vm.item.set("temporary", true); vm.item.set("cpu_count", 1); vm.item.set("memory_mb", 384); - vm.item.set("prepare_lend_mthp", false); + vm.item.set(LendMthpMode.KEY, LendMthpMode.DISABLED); var boot = BootConfig.of(vm); boot.setProtocol(BootConfig.Protocol.LINUX); boot.setLinuxSource(BootConfig.LinuxSource.MANUAL); diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/basic/VMEditBasicTab.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/basic/VMEditBasicTab.java index 607daa45c..8ab148be2 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/basic/VMEditBasicTab.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/basic/VMEditBasicTab.java @@ -15,6 +15,7 @@ import cn.classfun.droidvm.R; import cn.classfun.droidvm.lib.store.base.DataItem; +import cn.classfun.droidvm.lib.store.vm.LendMthpMode; import cn.classfun.droidvm.lib.store.vm.ProtectedVM; import cn.classfun.droidvm.lib.store.vm.VMBackend; import cn.classfun.droidvm.lib.size.SizeUnit; @@ -39,7 +40,7 @@ public final class VMEditBasicTab extends VMEditBaseTab { private SwitchRowWidget swUsb; private SwitchRowWidget swSandbox; private SwitchRowWidget swHugepages; - private SwitchRowWidget swPrepareLendMthp; + private ChooseRowWidget choosePrepareLendMthp; private ChooseRowWidget chooseProtectedVm; private ChooseRowWidget chooseBackend; private ChooseRowWidget chooseHypervisor; @@ -62,7 +63,7 @@ public void initView() { swUsb = view.findViewById(R.id.sw_usb); swSandbox = view.findViewById(R.id.sw_sandbox); swHugepages = view.findViewById(R.id.sw_hugepages); - swPrepareLendMthp = view.findViewById(R.id.sw_prepare_lend_mthp); + choosePrepareLendMthp = view.findViewById(R.id.choose_prepare_lend_mthp); chooseProtectedVm = view.findViewById(R.id.choose_protected_vm); chooseBackend = view.findViewById(R.id.choose_backend); chooseHypervisor = view.findViewById(R.id.choose_hypervisor); @@ -74,6 +75,7 @@ public void initValue() { inputMemory.setValue(512, SizeUnit.MB); inputCpu.setValue(1); inputSwiotlb.setValue(64, SizeUnit.MB); + choosePrepareLendMthp.configure(LendMthpMode.class, LendMthpMode.DEFAULT); chooseProtectedVm.configure(ProtectedVM.class, PROTECTED_WITHOUT_FIRMWARE); chooseBackend.configure(VMBackend.class, VMBackend.DEFAULT); chooseHypervisor.configure(VMHypervisor.class, VMHypervisor.DEFAULT); @@ -93,7 +95,7 @@ public void loadConfig(@NonNull VMConfig config) { swUsb.setChecked(item.optBoolean("usb", false)); swSandbox.setChecked(item.optBoolean("sandbox", false)); swHugepages.setChecked(item.optBoolean("hugepages", false)); - swPrepareLendMthp.setChecked(item.optBoolean("prepare_lend_mthp", true)); + choosePrepareLendMthp.setSelectedItem(LendMthpMode.fromItem(item)); chooseProtectedVm.setSelectedItem(optEnum(item, "protected_vm", PROTECTED_WITHOUT_FIRMWARE)); chooseBackend.setSelectedItem(optEnum(item, "backend", VMBackend.DEFAULT)); chooseHypervisor.setSelectedItem(optEnum(item, "hypervisor", VMHypervisor.DEFAULT)); @@ -187,7 +189,8 @@ public void saveConfig(@NonNull VMConfig config) { item.set("usb", swUsb.isChecked()); item.set("sandbox", swSandbox.isChecked()); item.set("hugepages", swHugepages.isChecked()); - item.set("prepare_lend_mthp", swPrepareLendMthp.isChecked()); + LendMthpMode lendMthpMode = choosePrepareLendMthp.getSelectedItem(); + item.set(LendMthpMode.KEY, lendMthpMode); ProtectedVM pvm = chooseProtectedVm.getSelectedItem(); item.set("protected_vm", pvm); VMBackend backend = chooseBackend.getSelectedItem(); diff --git a/app/src/main/res/layout/partial_vm_edit_basic.xml b/app/src/main/res/layout/partial_vm_edit_basic.xml index 6dc7aef43..7faec6450 100644 --- a/app/src/main/res/layout/partial_vm_edit_basic.xml +++ b/app/src/main/res/layout/partial_vm_edit_basic.xml @@ -103,11 +103,10 @@ android:icon="@drawable/ic_memory" android:text="@string/create_vm_hugepages" /> - diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 08f44a003..6cb8f6f1f 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -181,6 +181,9 @@ 沙箱 大页内存 预分配大页 + 禁用 + 单次 (整块借出) + 分块 (≤256MiB) 普通 保护 无固件保护 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2fe723817..9cb7f5594 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -183,6 +183,9 @@ Sandbox Hugepages Prepare Lend mTHP + Disabled + Single (whole region) + Chunked (≤256MiB) Normal Protected Protected without firmware From c13abfbdc7638d7d3568e58db6b23fbfbf552ef5 Mon Sep 17 00:00:00 2001 From: BigfootACA Date: Fri, 26 Jun 2026 11:08:42 +0800 Subject: [PATCH 046/182] cpp: add libcompat for old android version supports Signed-off-by: BigfootACA --- app/build.gradle.kts | 10 +- app/src/main/cpp/CMakeLists.txt | 25 +++- app/src/main/cpp/compat/a13.S | 1 + app/src/main/cpp/compat/a13.cpp | 19 +++ app/src/main/cpp/compat/a14.S | 28 ++++ app/src/main/cpp/compat/a14.cpp | 139 ++++++++++++++++++ app/src/main/cpp/compat/a15.S | 1 + app/src/main/cpp/compat/a15.cpp | 1 + app/src/main/cpp/compat/a16.S | 1 + app/src/main/cpp/compat/a16.cpp | 1 + app/src/main/cpp/compat/a17.S | 0 app/src/main/cpp/compat/a17.cpp | 0 .../droidvm/daemon/vm/VMBackendInstance.java | 4 + .../cn/classfun/droidvm/lib/Constants.java | 10 +- 14 files changed, 237 insertions(+), 3 deletions(-) create mode 100644 app/src/main/cpp/compat/a13.S create mode 100644 app/src/main/cpp/compat/a13.cpp create mode 100644 app/src/main/cpp/compat/a14.S create mode 100644 app/src/main/cpp/compat/a14.cpp create mode 100644 app/src/main/cpp/compat/a15.S create mode 100644 app/src/main/cpp/compat/a15.cpp create mode 100644 app/src/main/cpp/compat/a16.S create mode 100644 app/src/main/cpp/compat/a16.cpp create mode 100644 app/src/main/cpp/compat/a17.S create mode 100644 app/src/main/cpp/compat/a17.cpp diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 652d528eb..76309e640 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -138,7 +138,15 @@ abstract class CopyNativeBinAssetsTask : DefaultTask() { dest.parentFile.mkdirs() src.copyTo(dest, overwrite = true) } - val libraries = setOf("libsimpledump.so", "libunixhelper.so") + val libraries = setOf( + "libsimpledump.so", + "libunixhelper.so", + "libcompat_a13.so", + "libcompat_a14.so", + "libcompat_a15.so", + "libcompat_a16.so", + "libcompat_a17.so", + ) cmakeDir.walkTopDown() .filter { it.name in libraries && it.isFile } .forEach { src -> diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index 67d76204a..d7a0eca90 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.22.1) -project(droidvm LANGUAGES C CXX) +project(droidvm LANGUAGES C CXX ASM) set(SHARED_INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/jsoncpp/include @@ -61,6 +61,29 @@ add_library(simpledump SHARED target_compile_options(simpledump PRIVATE -Wall -Wextra -O0 -Wno-format-security -Wno-format) +add_library(compat_a13 SHARED compat/a13.cpp compat/a13.S) +add_library(compat_a14 SHARED compat/a14.cpp compat/a14.S) +add_library(compat_a15 SHARED compat/a15.cpp compat/a15.S) +add_library(compat_a16 SHARED compat/a16.cpp compat/a16.S) +add_library(compat_a17 SHARED compat/a17.cpp compat/a17.S) +target_compile_options(compat_a13 PRIVATE -Wall -Wextra -O0 -Wno-nullability-completeness) +target_compile_options(compat_a14 PRIVATE -Wall -Wextra -O0 -Wno-nullability-completeness) +target_compile_options(compat_a15 PRIVATE -Wall -Wextra -O0 -Wno-nullability-completeness) +target_compile_options(compat_a16 PRIVATE -Wall -Wextra -O0 -Wno-nullability-completeness) +target_compile_options(compat_a17 PRIVATE -Wall -Wextra -O0 -Wno-nullability-completeness) +target_link_options(compat_a13 PRIVATE + "-Wl,--undefined=_ZTVNSt6__ndk114basic_ofstreamIcNS_11char_traitsIcEEEE" + "-Wl,--undefined=_ZTTNSt6__ndk114basic_ofstreamIcNS_11char_traitsIcEEEE" + "-Wl,--defsym,_ZTVNSt3__114basic_ofstreamIcNS_11char_traitsIcEEEE=_ZTVNSt6__ndk114basic_ofstreamIcNS_11char_traitsIcEEEE" + "-Wl,--defsym,_ZTTNSt3__114basic_ofstreamIcNS_11char_traitsIcEEEE=_ZTTNSt6__ndk114basic_ofstreamIcNS_11char_traitsIcEEEE" +) +target_link_options(compat_a14 PRIVATE + "-Wl,--undefined=_ZTVNSt6__ndk114basic_ofstreamIcNS_11char_traitsIcEEEE" + "-Wl,--undefined=_ZTTNSt6__ndk114basic_ofstreamIcNS_11char_traitsIcEEEE" + "-Wl,--defsym,_ZTVNSt3__114basic_ofstreamIcNS_11char_traitsIcEEEE=_ZTVNSt6__ndk114basic_ofstreamIcNS_11char_traitsIcEEEE" + "-Wl,--defsym,_ZTTNSt3__114basic_ofstreamIcNS_11char_traitsIcEEEE=_ZTTNSt6__ndk114basic_ofstreamIcNS_11char_traitsIcEEEE" +) + set(LIBVNCSERVER_DIR ${CMAKE_CURRENT_SOURCE_DIR}/libvncserver) set(LIBVNCSERVER_INSTALL OFF CACHE BOOL "" FORCE) set(WITH_OPENSSL ON CACHE BOOL "" FORCE) diff --git a/app/src/main/cpp/compat/a13.S b/app/src/main/cpp/compat/a13.S new file mode 100644 index 000000000..8e60f3d81 --- /dev/null +++ b/app/src/main/cpp/compat/a13.S @@ -0,0 +1 @@ +#include "a14.S" diff --git a/app/src/main/cpp/compat/a13.cpp b/app/src/main/cpp/compat/a13.cpp new file mode 100644 index 000000000..11fb5abe3 --- /dev/null +++ b/app/src/main/cpp/compat/a13.cpp @@ -0,0 +1,19 @@ +#include "a14.cpp" +#include +#include +#include +#include +#include +#include +#include +#include + +__attribute__((weak)) +extern "C" int32_t ANativeWindow_readFromParcel(const void* _Nonnull, void* _Nullable* _Nonnull) { + return (int32_t)0x80000002; +} + +__attribute__((weak)) +extern "C" int32_t ANativeWindow_writeToParcel(void* _Nonnull, void* _Nonnull) { + return (int32_t)0x80000002; +} diff --git a/app/src/main/cpp/compat/a14.S b/app/src/main/cpp/compat/a14.S new file mode 100644 index 000000000..b3f50afb3 --- /dev/null +++ b/app/src/main/cpp/compat/a14.S @@ -0,0 +1,28 @@ +#include "a15.S" +#if defined(__arm64__) || defined(__aarch64__) +.text + +// std::__ndk1::basic_ofstream >::open(std::__ndk1::basic_string, std::__ndk1::allocator > const&, unsigned int) +.globl _ZNSt3__114basic_ofstreamIcNS_11char_traitsIcEEE4openERKNS_12basic_stringIcS2_NS_9allocatorIcEEEEj +.type _ZNSt3__114basic_ofstreamIcNS_11char_traitsIcEEE4openERKNS_12basic_stringIcS2_NS_9allocatorIcEEEEj, %function +_ZNSt3__114basic_ofstreamIcNS_11char_traitsIcEEE4openERKNS_12basic_stringIcS2_NS_9allocatorIcEEEEj: + b _ZNSt6__ndk114basic_ofstreamIcNS_11char_traitsIcEEE4openERKNS_12basic_stringIcS2_NS_9allocatorIcEEEEj + +// std::__1::basic_filebuf >::~basic_filebuf() +.globl _ZNSt3__113basic_filebufIcNS_11char_traitsIcEEED1Ev +.type _ZNSt3__113basic_filebufIcNS_11char_traitsIcEEED1Ev, %function +_ZNSt3__113basic_filebufIcNS_11char_traitsIcEEED1Ev: + b _ZNSt6__ndk113basic_filebufIcNS_11char_traitsIcEEED1Ev + +// std::__1::basic_filebuf >::basic_filebuf() +.globl _ZNSt3__113basic_filebufIcNS_11char_traitsIcEEEC1Ev +.type _ZNSt3__113basic_filebufIcNS_11char_traitsIcEEEC1Ev, %function +_ZNSt3__113basic_filebufIcNS_11char_traitsIcEEEC1Ev: + b _ZNSt6__ndk113basic_filebufIcNS_11char_traitsIcEEEC1Ev + +// std::__1::basic_filebuf >::close() +.globl _ZNSt3__113basic_filebufIcNS_11char_traitsIcEEE5closeEv +.type _ZNSt3__113basic_filebufIcNS_11char_traitsIcEEE5closeEv, %function +_ZNSt3__113basic_filebufIcNS_11char_traitsIcEEE5closeEv: + b _ZNSt6__ndk113basic_filebufIcNS_11char_traitsIcEEE5closeEv +#endif diff --git a/app/src/main/cpp/compat/a14.cpp b/app/src/main/cpp/compat/a14.cpp new file mode 100644 index 000000000..84a8775b3 --- /dev/null +++ b/app/src/main/cpp/compat/a14.cpp @@ -0,0 +1,139 @@ +#include "a15.cpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static std::ofstream ofs; + +__attribute__((weak)) +extern "C" void AIBinder_Class_setTransactionCodeToFunctionNameMap(void*, const char* const*, size_t) { + +} + +__attribute__((weak)) +void __libcpp_verbose_abort(char const* format, ...) __asm("_ZNSt3__122__libcpp_verbose_abortEPKcz"); + +__attribute__((weak)) +void __libcpp_verbose_abort(char const* format, ...) { + va_list list; + va_start(list, format); + vfprintf(stderr, format, list); + va_end(list); + abort(); +} + +std::to_chars_result fix_to_chars1(char* first, char* last, float value,std::chars_format fmt, int precision) __asm("_ZNSt3__18to_charsEPcS0_fNS_12chars_formatEi"); + +__attribute__((weak)) +std::to_chars_result fix_to_chars1(char* first, char* last, float value,std::chars_format fmt, int precision) { + return std::to_chars(first, last, value, fmt, precision); +} + +std::to_chars_result fix_to_chars2(char* first, char* last, float value) __asm("_ZNSt3__18to_charsEPcS0_f"); + +__attribute__((weak)) +std::to_chars_result fix_to_chars2(char* first, char* last, float value) { + return std::to_chars(first, last, value); +} + +std::to_chars_result fix_to_chars3(char* first, char* last, float value,std::chars_format fmt) __asm("_ZNSt3__18to_charsEPcS0_fNS_12chars_formatE"); + +__attribute__((weak)) +std::to_chars_result fix_to_chars3(char* first, char* last, float value,std::chars_format fmt) { + return std::to_chars(first, last, value, fmt); +} + +std::to_chars_result fix_to_chars4(char* first, char* last, double value,std::chars_format fmt, int precision) __asm("_ZNSt3__18to_charsEPcS0_dNS_12chars_formatEi"); + +__attribute__((weak)) +std::to_chars_result fix_to_chars4(char* first, char* last, double value,std::chars_format fmt, int precision) { + return std::to_chars(first, last, value, fmt, precision); +} + +std::to_chars_result fix_to_chars5(char* first, char* last, double value) __asm("_ZNSt3__18to_charsEPcS0_d"); + +__attribute__((weak)) +std::to_chars_result fix_to_chars5(char* first, char* last, double value) { + return std::to_chars(first, last, value); +} + +std::to_chars_result fix_to_chars6(char* first, char* last, double value,std::chars_format fmt) __asm("_ZNSt3__18to_charsEPcS0_dNS_12chars_formatE"); + +__attribute__((weak)) +std::to_chars_result fix_to_chars6(char* first, char* last, double value,std::chars_format fmt) { + return std::to_chars(first, last, value, fmt); +} + +std::to_chars_result fix_to_chars7(char* first, char* last, long double value,std::chars_format fmt, int precision) __asm("_ZNSt3__18to_charsEPcS0_eNS_12chars_formatEi"); + +__attribute__((weak)) +std::to_chars_result fix_to_chars7(char* first, char* last, long double value,std::chars_format fmt, int precision) { + return std::to_chars(first, last, value, fmt, precision); +} + +std::to_chars_result fix_to_chars8(char* first, char* last, long double value) __asm("_ZNSt3__18to_charsEPcS0_e"); + +__attribute__((weak)) +std::to_chars_result fix_to_chars8(char* first, char* last, long double value) { + return std::to_chars(first, last, value); +} + +std::to_chars_result fix_to_chars9(char* first, char* last, long double value,std::chars_format fmt) __asm("_ZNSt3__18to_charsEPcS0_eNS_12chars_formatE"); + +__attribute__((weak)) +std::to_chars_result fix_to_chars9(char* first, char* last, long double value,std::chars_format fmt) { + return std::to_chars(first, last, value, fmt); +} + +namespace android { + typedef int64_t PictureProfileId; + class PictureProfileHandle { + public: + static const PictureProfileHandle NONE; + PictureProfileHandle() { *this = NONE; } + explicit PictureProfileHandle(PictureProfileId id) : mId(id) {} + PictureProfileId const& getId() const { return mId; } + inline bool operator==(const PictureProfileHandle& rhs) { return mId == rhs.mId; } + inline bool operator!=(const PictureProfileHandle& rhs) { return !(*this == rhs); } + inline bool operator!() const { return mId == NONE.mId; } + operator bool() const { return !!*this; } + friend ::std::string toString(const PictureProfileHandle& handle); + private: + PictureProfileId mId; + }; + const PictureProfileHandle PictureProfileHandle::NONE(0); + ::std::string toString(const PictureProfileHandle& handle) { + return std::format("{:#010x}", handle.getId()); + } + namespace binder::os { + __attribute__((weak)) + void trace_begin(uint64_t, const char*) { + } + __attribute__((weak)) + void trace_end(uint64_t) { + } + __attribute__((weak)) + void trace_int(uint64_t, const char*, int32_t) { + } + } +} + +__attribute__((weak)) +extern "C" int epoll_pwait2( + int epfd, struct epoll_event* events, int maxevents, + const struct timespec *_Nullable ts, + const sigset_t *_Nullable sigmask +) { + int ms = -1; + if (ts) { + ms = (int)(ts->tv_sec * 1000L + ts->tv_nsec / 1000000L); + if (ms < 0) ms = 0; + } + return epoll_pwait(epfd, events, maxevents, ms, sigmask); +} diff --git a/app/src/main/cpp/compat/a15.S b/app/src/main/cpp/compat/a15.S new file mode 100644 index 000000000..a23f8b99b --- /dev/null +++ b/app/src/main/cpp/compat/a15.S @@ -0,0 +1 @@ +#include "a16.S" diff --git a/app/src/main/cpp/compat/a15.cpp b/app/src/main/cpp/compat/a15.cpp new file mode 100644 index 000000000..b7c6b1aac --- /dev/null +++ b/app/src/main/cpp/compat/a15.cpp @@ -0,0 +1 @@ +#include "a16.cpp" diff --git a/app/src/main/cpp/compat/a16.S b/app/src/main/cpp/compat/a16.S new file mode 100644 index 000000000..b1093e154 --- /dev/null +++ b/app/src/main/cpp/compat/a16.S @@ -0,0 +1 @@ +#include "a17.S" diff --git a/app/src/main/cpp/compat/a16.cpp b/app/src/main/cpp/compat/a16.cpp new file mode 100644 index 000000000..08479010b --- /dev/null +++ b/app/src/main/cpp/compat/a16.cpp @@ -0,0 +1 @@ +#include "a17.cpp" diff --git a/app/src/main/cpp/compat/a17.S b/app/src/main/cpp/compat/a17.S new file mode 100644 index 000000000..e69de29bb diff --git a/app/src/main/cpp/compat/a17.cpp b/app/src/main/cpp/compat/a17.cpp new file mode 100644 index 000000000..e69de29bb diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMBackendInstance.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMBackendInstance.java index e9ead2135..73775521e 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMBackendInstance.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMBackendInstance.java @@ -3,8 +3,11 @@ import static cn.classfun.droidvm.lib.Constants.DATA_DIR; import static cn.classfun.droidvm.lib.natives.NativeProcess.RLIM_INFINITY; import static cn.classfun.droidvm.lib.utils.RunUtils.run; +import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; import static cn.classfun.droidvm.lib.utils.StringUtils.pathJoin; +import android.os.Build; + import androidx.annotation.NonNull; import java.util.LinkedHashMap; @@ -54,6 +57,7 @@ private void cleanUpMemory() { protected void prepareProcess(@NonNull NativeProcess.Builder builder) { String[] preload = { pathJoin(DATA_DIR, "lib", "libsimpledump.so"), + pathJoin(DATA_DIR, "lib", fmt("libcompat_a%s.so", Build.VERSION.RELEASE)), }; builder.environment("LD_PRELOAD", String.join(":", preload)); builder.environment("LD_LIBRARY_PATH", pathJoin(DATA_DIR, "usr", "lib")); diff --git a/app/src/main/java/cn/classfun/droidvm/lib/Constants.java b/app/src/main/java/cn/classfun/droidvm/lib/Constants.java index 8642176af..f15802dd9 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/Constants.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/Constants.java @@ -24,7 +24,15 @@ public final class Constants { // third-party daemons (gvswitch/pbridge/bridgedhcp/netbox) and lbx now // arrive via the prebuilt tar.xz, extracted to DATA/bin/. public static final String[] BINARIES_BUILT = {"droidvm", "daemon"}; - public static final String[] LIBRARIES_BUILT = {"libsimpledump.so", "libunixhelper.so"}; + public static final String[] LIBRARIES_BUILT = { + "libsimpledump.so", + "libunixhelper.so", + "libcompat_a13.so", + "libcompat_a14.so", + "libcompat_a15.so", + "libcompat_a16.so", + "libcompat_a17.so", + }; /** * IFA_RT_PRIORITY magic that pbridge tags its ND/ARP-offload proxy * addresses with (its --offload-workaround-magic). Host-IP discovery From ce5f41ad861d9aaff4e4b141b45ca0225ddf3483 Mon Sep 17 00:00:00 2001 From: BigfootACA Date: Fri, 26 Jun 2026 11:10:12 +0800 Subject: [PATCH 047/182] Daemon: set proctitile to droidvmd Signed-off-by: BigfootACA --- app/src/main/java/cn/classfun/droidvm/daemon/Daemon.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/Daemon.java b/app/src/main/java/cn/classfun/droidvm/daemon/Daemon.java index a96697f9b..080d668dc 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/Daemon.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/Daemon.java @@ -212,6 +212,14 @@ private static String getMyHash() { } } + private static void setProcTitle() { + try { + FileUtils.writeFile("/proc/self/comm", "droidvmd"); + } catch (IOException e) { + Log.w(TAG, "Failed to set process title", e); + } + } + public static void main(String... args) { boolean force = Arrays.asList(args).contains("--force"); System.out.print("Starting DroidVM Daemon...\n"); @@ -219,6 +227,7 @@ public static void main(String... args) { // Build a system Context on the main thread (it prepares the main looper) so the daemon can // broadcast the native-display binder to the UI. Must happen before server.run() blocks here. DaemonSystemContext.init(); + setProcTitle(); System.out.printf("Current pid: %d\n", Process.myPid()); Log.d(TAG, "DroidVM Daemon is starting..."); if (!acquireLock(force)) { From f23c5c0b821094f53191587c20f7cbe0c3435296 Mon Sep 17 00:00:00 2001 From: BigfootACA Date: Fri, 26 Jun 2026 11:10:29 +0800 Subject: [PATCH 048/182] assets: remove old hash.json Signed-off-by: BigfootACA --- app/src/main/assets/hash.json | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 app/src/main/assets/hash.json diff --git a/app/src/main/assets/hash.json b/app/src/main/assets/hash.json deleted file mode 100644 index c62f5d047..000000000 --- a/app/src/main/assets/hash.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "hash": [ - { - "file": "prebuilts/prebuilt-arm64-v8a.7z", - "sha256": "263e905b4d33fecdaba1f0744384ff6bc4b7b478e65ecafd6ac8a50b0eeec55c" - }, { - "file": "bin/arm64-v8a/7za", - "sha256": "8443af0bd27d694f0431c0e46fc33d149004e5d9f8c03d484cffb8b03e928531" - }, { - "file": "bin/x86_64/7za", - "sha256": "57ea4f617b559556a4019ac8fe8c27cc9422ff4a988ad1be3728319a622093d8" - } - ] -} From 42e5f8ec33ea5e9b7526f844c14ad23f7098c9c5 Mon Sep 17 00:00:00 2001 From: BigfootACA Date: Fri, 26 Jun 2026 11:12:35 +0800 Subject: [PATCH 049/182] DaemonHelper: fix stale pid file after reboot Signed-off-by: BigfootACA --- .../droidvm/lib/daemon/DaemonHelper.java | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/cn/classfun/droidvm/lib/daemon/DaemonHelper.java b/app/src/main/java/cn/classfun/droidvm/lib/daemon/DaemonHelper.java index bd4ae2480..0ff2b882c 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/daemon/DaemonHelper.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/daemon/DaemonHelper.java @@ -75,10 +75,26 @@ public String getDaemonZipPath() { public static int readPid() { try { var pid = parseInt(shellReadFile(getPidFile()).trim()); - return isPidExists(pid) ? pid : -1; + if (isPidExists(pid)) { + try { + var comm = shellReadFile(fmt("/proc/%d/comm", pid)).trim(); + if (comm.equals("droidvmd")) return pid; + Log.i(TAG, fmt("proctitle mismatch: %s", comm)); + } catch (Exception e) { + Log.w(TAG, "Failed to read proctitle", e); + } + try { + var comm = shellReadFile(fmt("/proc/%d/cmdline", pid)); + if (comm.contains(Daemon.class.getName())) return pid; + Log.i(TAG, fmt("cmdline mismatch: %s", comm.replace('\0', ' '))); + } catch (Exception e) { + Log.w(TAG, "Failed to read cmdline", e); + } + Log.w(TAG, "pid file maybe stale"); + } } catch (Exception ignored) { - return -1; } + return -1; } public static boolean isDaemonRunning() { From 4f516294fce3d32f24cb2fcde51ae2a9760268ee Mon Sep 17 00:00:00 2001 From: BigfootACA Date: Fri, 26 Jun 2026 11:45:17 +0800 Subject: [PATCH 050/182] dialog_logs.xml: fix dialog button display Signed-off-by: BigfootACA --- .../droidvm/lib/daemon/VMEventHandler.java | 11 +++----- .../info/snapshot/DiskInfoSnapshotTab.java | 10 +++----- app/src/main/res/layout/dialog_logs.xml | 25 ++++++++----------- 3 files changed, 19 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/cn/classfun/droidvm/lib/daemon/VMEventHandler.java b/app/src/main/java/cn/classfun/droidvm/lib/daemon/VMEventHandler.java index 4d679c7b0..28b602b40 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/daemon/VMEventHandler.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/daemon/VMEventHandler.java @@ -18,7 +18,6 @@ import android.os.Handler; import android.os.Looper; import android.util.Log; -import android.view.LayoutInflater; import android.widget.TextView; import android.widget.Toast; @@ -104,16 +103,14 @@ private void showExitDialog(Activity act, String vmName, int exitCode, @NonNull store.load(act); var vm = store.findByName(vmName); if (vm != null && vm.item.optBoolean("temporary", false)) return; - var inf = LayoutInflater.from(act); - var view = inf.inflate(R.layout.dialog_logs, null); - TextView tvLog = view.findViewById(R.id.tv_log); - tvLog.setText(logText.toString().trim()); - new MaterialAlertDialogBuilder(act) + var dialog = new MaterialAlertDialogBuilder(act) .setTitle(act.getString(R.string.vm_exit_dialog_title, vmName)) .setMessage(act.getString(R.string.vm_exit_dialog_message, exitCode)) - .setView(view) + .setView(R.layout.dialog_logs) .setPositiveButton(android.R.string.ok, null) .show(); + TextView tvLog = dialog.findViewById(R.id.tv_log); + if (tvLog != null) tvLog.append(logText.toString().trim()); } private void addExitHandler() { diff --git a/app/src/main/java/cn/classfun/droidvm/ui/disk/info/snapshot/DiskInfoSnapshotTab.java b/app/src/main/java/cn/classfun/droidvm/ui/disk/info/snapshot/DiskInfoSnapshotTab.java index e55cccc01..465057d97 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/disk/info/snapshot/DiskInfoSnapshotTab.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/disk/info/snapshot/DiskInfoSnapshotTab.java @@ -271,15 +271,13 @@ private void runSnapshotCommand(@NonNull String flag, @NonNull String snapshotNa } private void showErrorDialog(@NonNull String message) { - var dialogView = LayoutInflater.from(activity) - .inflate(R.layout.dialog_logs, null); - TextView tvLog = dialogView.findViewById(R.id.tv_log); - tvLog.setText(message); - new MaterialAlertDialogBuilder(activity) + var dialog = new MaterialAlertDialogBuilder(activity) .setTitle(R.string.disk_info_snapshot_failed_title) - .setView(dialogView) + .setView(R.layout.dialog_logs) .setPositiveButton(android.R.string.ok, null) .show(); + TextView tvLog = dialog.findViewById(R.id.tv_log); + if (tvLog != null) tvLog.append(message.trim()); } } diff --git a/app/src/main/res/layout/dialog_logs.xml b/app/src/main/res/layout/dialog_logs.xml index d4f1f30ab..4914b5205 100644 --- a/app/src/main/res/layout/dialog_logs.xml +++ b/app/src/main/res/layout/dialog_logs.xml @@ -1,16 +1,13 @@ - - - - - + android:layout_height="wrap_content" + android:contentDescription="@string/logs_title" + android:fontFamily="monospace" + android:gravity="bottom" + android:maxLines="15" + android:padding="16dp" + android:scrollbars="vertical" + android:textIsSelectable="true" + android:textSize="12sp" /> From fee9819e31676dabdfaf349d8385b6e2004f7d47 Mon Sep 17 00:00:00 2001 From: BigfootACA Date: Fri, 26 Jun 2026 12:27:15 +0800 Subject: [PATCH 051/182] LogHelper: add display delay Signed-off-by: BigfootACA --- .../main/java/cn/classfun/droidvm/lib/diag/LogHelper.java | 6 +++++- .../java/cn/classfun/droidvm/lib/diag/LogHelperHandler.java | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/cn/classfun/droidvm/lib/diag/LogHelper.java b/app/src/main/java/cn/classfun/droidvm/lib/diag/LogHelper.java index c039d5d9f..1f347e817 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/diag/LogHelper.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/diag/LogHelper.java @@ -1,5 +1,8 @@ package cn.classfun.droidvm.lib.diag; +import android.os.Handler; +import android.os.Looper; + import androidx.annotation.NonNull; import org.json.JSONObject; @@ -70,7 +73,8 @@ public void onDaemonEvent(@NonNull JSONObject msg) { for (var handler : handlers) { if (logs.disabled.contains(handler)) continue; if (!handler.match(vmId, stream, full)) continue; - vmEventHandler.queueActivityTask(act -> handler.show(act, vmId, vmName)); + Runnable show = () -> vmEventHandler.queueActivityTask(act -> handler.show(act, vmId, vmName)); + new Handler(Looper.getMainLooper()).postDelayed(show, handler.getShowDelay().toMillis()); if (handler.isOnce()) logs.disabled.add(handler); } diff --git a/app/src/main/java/cn/classfun/droidvm/lib/diag/LogHelperHandler.java b/app/src/main/java/cn/classfun/droidvm/lib/diag/LogHelperHandler.java index 2c7028879..e551bbcc7 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/diag/LogHelperHandler.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/diag/LogHelperHandler.java @@ -12,6 +12,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import java.time.Duration; import java.util.UUID; import cn.classfun.droidvm.R; @@ -27,6 +28,10 @@ public boolean isOnce() { return true; } + public Duration getShowDelay() { + return Duration.ofSeconds(2); + } + public abstract void show(@NonNull Context ctx, @NonNull UUID vmId, @NonNull String vmName); protected static boolean isGunyah() { From 3b8fdd6c15f6389f634ee4afc4c418d9def7ef8c Mon Sep 17 00:00:00 2001 From: BigfootACA Date: Fri, 26 Jun 2026 12:50:43 +0800 Subject: [PATCH 052/182] VMEventHandler: add see more button in vm exit dialog Signed-off-by: BigfootACA --- .../droidvm/lib/daemon/VMEventHandler.java | 15 +++++++++++++-- app/src/main/res/values-zh-rCN/strings.xml | 1 + app/src/main/res/values-zh-rTW/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/cn/classfun/droidvm/lib/daemon/VMEventHandler.java b/app/src/main/java/cn/classfun/droidvm/lib/daemon/VMEventHandler.java index 28b602b40..855be845f 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/daemon/VMEventHandler.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/daemon/VMEventHandler.java @@ -42,6 +42,7 @@ import cn.classfun.droidvm.lib.ui.UIContext; import cn.classfun.droidvm.ui.SplashActivity; import cn.classfun.droidvm.ui.main.MainActivity; +import cn.classfun.droidvm.ui.vm.console.VMConsoleActivity; public final class VMEventHandler implements DaemonConnection.EventListener, @@ -102,12 +103,22 @@ private void showExitDialog(Activity act, String vmName, int exitCode, @NonNull var store = new VMStore(); store.load(act); var vm = store.findByName(vmName); - if (vm != null && vm.item.optBoolean("temporary", false)) return; + if (vm == null) return; + if (vm.item.optBoolean("temporary", false)) return; + DialogInterface.OnClickListener btnSeeMore = (d, w) -> { + if (act.isFinishing() || act.isDestroyed()) return; + var intent = new Intent(act, VMConsoleActivity.class); + intent.putExtra(VMConsoleActivity.EXTRA_VM_ID, vm.getId().toString()); + intent.putExtra(VMConsoleActivity.EXTRA_VM_NAME, vmName); + intent.putExtra(VMConsoleActivity.EXTRA_STREAM, "stdio"); + intent.putExtra(VMConsoleActivity.EXTRA_LOGS, true); + act.startActivity(intent); + }; var dialog = new MaterialAlertDialogBuilder(act) .setTitle(act.getString(R.string.vm_exit_dialog_title, vmName)) .setMessage(act.getString(R.string.vm_exit_dialog_message, exitCode)) .setView(R.layout.dialog_logs) - .setPositiveButton(android.R.string.ok, null) + .setPositiveButton(R.string.vm_exit_see_more, btnSeeMore) .show(); TextView tvLog = dialog.findViewById(R.id.tv_log); if (tvLog != null) tvLog.append(logText.toString().trim()); diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 6cb8f6f1f..fde9c3f0a 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -112,6 +112,7 @@ 虚拟机"%1$s"启动失败 虚拟机以退出码 %1$d 退出。\n\n日志: 无可用日志。 + 查看更多 启动 停止 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 8a6eddb06..ac7ed8b6b 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -112,6 +112,7 @@ 虛擬機器"%1$s"啟動失敗 虛擬機器以退出碼 %1$d 退出。\n\n日誌: 無可用日誌。 + 查看更多 啟動 停止 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9cb7f5594..d14f63995 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -113,6 +113,7 @@ VM \"%1$s\" Failed The virtual machine exited with code %1$d.\n\nLogs: No logs available. + See more Start Stop From 7412d0038701a44d4152626233a4459a9957a82e Mon Sep 17 00:00:00 2001 From: BigfootACA Date: Fri, 26 Jun 2026 12:51:44 +0800 Subject: [PATCH 053/182] VMConsoleActivity: set toolbar menu button to white Signed-off-by: BigfootACA --- .../main/java/cn/classfun/droidvm/lib/ui/MaterialMenu.java | 4 +++- .../classfun/droidvm/ui/vm/console/VMConsoleActivity.java | 7 ++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/cn/classfun/droidvm/lib/ui/MaterialMenu.java b/app/src/main/java/cn/classfun/droidvm/lib/ui/MaterialMenu.java index f4269146d..2ac024dea 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/ui/MaterialMenu.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/ui/MaterialMenu.java @@ -190,7 +190,8 @@ private void applyListViewStyle() { listView.setDividerHeight(0); } - public static void setupToolbarMenu( + @NonNull + public static MenuItem setupToolbarMenu( @NonNull MaterialToolbar toolbar, @MenuRes int menuId, @Nullable MenuItem.OnMenuItemClickListener listener @@ -212,6 +213,7 @@ public static void setupToolbarMenu( } return false; }); + return item; } private static final class MenuEntry { diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/console/VMConsoleActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/console/VMConsoleActivity.java index ef0c73806..b3c8099bf 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/console/VMConsoleActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/console/VMConsoleActivity.java @@ -14,6 +14,9 @@ import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; import static cn.classfun.droidvm.lib.utils.ThreadUtils.runOnPool; +import android.content.res.ColorStateList; +import android.graphics.Color; +import android.graphics.PorterDuff; import android.net.Uri; import android.os.Bundle; import android.os.Handler; @@ -146,7 +149,9 @@ protected void onCreate(Bundle savedInstanceState) { MaterialToolbar toolbar = findViewById(R.id.toolbar); toolbar.setTitle(fmt("%s - %s", vmName, streamName)); toolbar.setNavigationOnClickListener(v -> finish()); - setupToolbarMenu(toolbar, R.menu.menu_vm_console, this::onMenuItemClicked); + var item = setupToolbarMenu(toolbar, R.menu.menu_vm_console, this::onMenuItemClicked); + item.setIconTintList(ColorStateList.valueOf(Color.WHITE)); + item.setIconTintMode(PorterDuff.Mode.SRC_IN); terminalView = findViewById(R.id.terminal_view); terminalView.setTerminalViewClient(viewClient); var consoleBin = getAssetBinaryPath("droidvm"); From 6e6aa09186d72e94015b355e275bfd9759f79fd7 Mon Sep 17 00:00:00 2001 From: BigfootACA Date: Fri, 26 Jun 2026 13:18:50 +0800 Subject: [PATCH 054/182] VMEditBasicTab: use single mTHP on Snapdragon 8 Gen 3 Signed-off-by: BigfootACA --- app/src/main/assets/data/gunyah.yaml | 1 + .../ui/vm/edit/basic/VMEditBasicTab.java | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/app/src/main/assets/data/gunyah.yaml b/app/src/main/assets/data/gunyah.yaml index c10a4d1f3..9886f2963 100644 --- a/app/src/main/assets/data/gunyah.yaml +++ b/app/src/main/assets/data/gunyah.yaml @@ -11,6 +11,7 @@ gunyah: - SM8650QAB capabilities: gunyah: yes + mthp_single: yes # Qualcomm Snapdragon 8 Elite - socs: diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/basic/VMEditBasicTab.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/basic/VMEditBasicTab.java index 8ab148be2..9a5bf6748 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/basic/VMEditBasicTab.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/basic/VMEditBasicTab.java @@ -6,6 +6,7 @@ import static cn.classfun.droidvm.lib.utils.StringUtils.getEditText; import android.text.TextUtils; +import android.util.Log; import android.view.View; import androidx.annotation.NonNull; @@ -14,6 +15,8 @@ import com.google.android.material.textfield.TextInputEditText; import cn.classfun.droidvm.R; +import cn.classfun.droidvm.lib.data.QcomChipName; +import cn.classfun.droidvm.lib.data.QcomGunyahSupports; import cn.classfun.droidvm.lib.store.base.DataItem; import cn.classfun.droidvm.lib.store.vm.LendMthpMode; import cn.classfun.droidvm.lib.store.vm.ProtectedVM; @@ -29,6 +32,7 @@ import cn.classfun.droidvm.ui.widgets.row.TextInputRowWidget; public final class VMEditBasicTab extends VMEditBaseTab { + private final String TAG = "VMEditBasicTab"; private TextInputRowWidget inputName; private TextInputRowWidget inputMemory; private TextInputRowWidget inputCpu; @@ -79,6 +83,19 @@ public void initValue() { chooseProtectedVm.configure(ProtectedVM.class, PROTECTED_WITHOUT_FIRMWARE); chooseBackend.configure(VMBackend.class, VMBackend.DEFAULT); chooseHypervisor.configure(VMHypervisor.class, VMHypervisor.DEFAULT); + choosePrepareLendMthp.configure(LendMthpMode.class, LendMthpMode.CHUNKED); + try { + var socModel = QcomChipName.getCurrentSoC(); + var gunyah = new QcomGunyahSupports(parent); + if (gunyah.isCapacitySupported(socModel, "no_mthp")) + choosePrepareLendMthp.setSelectedItem(LendMthpMode.DISABLED); + if (gunyah.isCapacitySupported(socModel, "mthp_chunked")) + choosePrepareLendMthp.setSelectedItem(LendMthpMode.CHUNKED); + if (gunyah.isCapacitySupported(socModel, "mthp_single")) + choosePrepareLendMthp.setSelectedItem(LendMthpMode.SINGLE); + } catch (Exception e) { + Log.w(TAG, "failed to load soc capacity", e); + } } @Override From 1cffe2108ac8a5c50ef29d8a6723431546194137 Mon Sep 17 00:00:00 2001 From: BigfootACA Date: Fri, 26 Jun 2026 17:05:46 +0800 Subject: [PATCH 055/182] VMEditBasicTab: add strace debug option Signed-off-by: BigfootACA --- .../droidvm/daemon/vm/VMBackendInstance.java | 47 +++++++++++++++++++ .../vm/backend/CrosvmBackendInstance.java | 4 +- .../vm/backend/QemuBackendInstance.java | 4 +- .../ui/vm/edit/basic/VMEditBasicTab.java | 5 +- .../main/res/layout/partial_vm_edit_basic.xml | 8 ++++ app/src/main/res/values-zh-rCN/strings.xml | 1 + app/src/main/res/values-zh-rTW/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 8 files changed, 66 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMBackendInstance.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMBackendInstance.java index 73775521e..86d5ea32b 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMBackendInstance.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMBackendInstance.java @@ -2,22 +2,35 @@ import static cn.classfun.droidvm.lib.Constants.DATA_DIR; import static cn.classfun.droidvm.lib.natives.NativeProcess.RLIM_INFINITY; +import static cn.classfun.droidvm.lib.utils.AssetUtils.getPrebuiltBinaryPath; import static cn.classfun.droidvm.lib.utils.RunUtils.run; import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; import static cn.classfun.droidvm.lib.utils.StringUtils.pathJoin; import android.os.Build; +import android.util.Log; import androidx.annotation.NonNull; +import java.io.File; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import cn.classfun.droidvm.daemon.console.ConsoleStream; import cn.classfun.droidvm.lib.natives.NativeProcess; +import cn.classfun.droidvm.lib.store.vm.VMConfig; +import cn.classfun.droidvm.lib.utils.FileUtils; public abstract class VMBackendInstance { + private static final String TAG = "VMBackendInstance"; protected final Map streams = new LinkedHashMap<>(); + protected final VMConfig config; + private boolean stracePrepared = false; + + protected VMBackendInstance(@NonNull VMConfig config) { + this.config = config; + } @NonNull public abstract VMStartResult start(); @@ -65,4 +78,38 @@ protected void prepareProcess(@NonNull NativeProcess.Builder builder) { builder.maxLockedMemory(RLIM_INFINITY); cleanUpMemory(); } + + @NonNull + protected String getStraceFolder() { + var dirName = fmt("strace-%s", config.getId()); + var dir = pathJoin(DATA_DIR, "cache", dirName); + var dirObj = new File(dir); + if (!stracePrepared) { + stracePrepared = true; + if (dirObj.exists()) + FileUtils.shellRemoveTree(dir); + if (!dirObj.mkdir()) + Log.w(TAG, "Failed to create strace directory"); + } + return dir; + } + + protected void prepareStraceArguments(@NonNull List args) { + var item = config.item; + if (!item.optBoolean("strace", false)) return; + var strace = getPrebuiltBinaryPath("strace"); + if (!new File(strace).exists()) + strace = FileUtils.findExecute("strace", null); + if (strace == null || !new File(strace).exists()) { + Log.e(TAG, "no strace binary found"); + return; + } + var dir = getStraceFolder(); + args.add(strace); + args.add("-Tttrvyyff"); + args.add("-s"); + args.add("8192"); + args.add("-o"); + args.add(pathJoin(dir, "strace.log")); + } } diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackendInstance.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackendInstance.java index 210a6db30..e66114e38 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackendInstance.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackendInstance.java @@ -51,7 +51,6 @@ public final class CrosvmBackendInstance extends VMBackendInstance { private static final String TAG = "CrosvmBackendInstance"; private static final String RUN_PATH = pathJoin(DATA_DIR, "run"); - private final VMConfig config; private SerialPipe uart = null; private String controlSocketPath = null; /** Owns the per-VM native-display input sockets (crosvm-facing + UI-facing); see start(). */ @@ -62,7 +61,7 @@ public final class CrosvmBackendInstance extends VMBackendInstance { private final SimpleConsoleStream stdioStream; public CrosvmBackendInstance(@NonNull VMConfig config) { - this.config = config; + super(config); uartStream = new FDPipeConsoleStream(config, "uart", -1, -1); stdoutStream = new InputConsoleStream(config, "stdout", null); stderrStream = new InputConsoleStream(config, "stderr", null); @@ -135,6 +134,7 @@ public VMStartResult start() { private List buildCommand() { var item = config.item; var args = new ArrayList(); + prepareStraceArguments(args); args.add(getPrebuiltBinaryPath("crosvm")); args.add("run"); args.add("--name"); diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/QemuBackendInstance.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/QemuBackendInstance.java index 6860a19e2..3897aa929 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/QemuBackendInstance.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/QemuBackendInstance.java @@ -47,7 +47,6 @@ public final class QemuBackendInstance extends VMBackendInstance { private static final String TAG = "QemuBackendInstance"; private static final String RUN_PATH = pathJoin(DATA_DIR, "run"); - private final VMConfig config; private String qmpSocketPath = null; private String uartSocketPath = null; private int ioThreadCounter = 0; @@ -58,7 +57,7 @@ public final class QemuBackendInstance extends VMBackendInstance { private final SimpleConsoleStream stdioStream; public QemuBackendInstance(@NonNull VMConfig config) { - this.config = config; + super(config); uartStream = new LocalSocketConsoleStream(config, "uart", null); stdoutStream = new InputConsoleStream(config, "stdout", null); stderrStream = new InputConsoleStream(config, "stderr", null); @@ -122,6 +121,7 @@ private void waitForUartClient() { private List buildCommand() { var item = config.item; var args = new ArrayList(); + prepareStraceArguments(args); args.add(getPrebuiltBinaryPath("qemu-system-aarch64")); args.add("-name"); args.add(config.getName()); diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/basic/VMEditBasicTab.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/basic/VMEditBasicTab.java index 9a5bf6748..80c0609fe 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/basic/VMEditBasicTab.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/basic/VMEditBasicTab.java @@ -44,6 +44,7 @@ public final class VMEditBasicTab extends VMEditBaseTab { private SwitchRowWidget swUsb; private SwitchRowWidget swSandbox; private SwitchRowWidget swHugepages; + private SwitchRowWidget swDebug; private ChooseRowWidget choosePrepareLendMthp; private ChooseRowWidget chooseProtectedVm; private ChooseRowWidget chooseBackend; @@ -67,6 +68,7 @@ public void initView() { swUsb = view.findViewById(R.id.sw_usb); swSandbox = view.findViewById(R.id.sw_sandbox); swHugepages = view.findViewById(R.id.sw_hugepages); + swDebug = view.findViewById(R.id.sw_debug); choosePrepareLendMthp = view.findViewById(R.id.choose_prepare_lend_mthp); chooseProtectedVm = view.findViewById(R.id.choose_protected_vm); chooseBackend = view.findViewById(R.id.choose_backend); @@ -79,7 +81,6 @@ public void initValue() { inputMemory.setValue(512, SizeUnit.MB); inputCpu.setValue(1); inputSwiotlb.setValue(64, SizeUnit.MB); - choosePrepareLendMthp.configure(LendMthpMode.class, LendMthpMode.DEFAULT); chooseProtectedVm.configure(ProtectedVM.class, PROTECTED_WITHOUT_FIRMWARE); chooseBackend.configure(VMBackend.class, VMBackend.DEFAULT); chooseHypervisor.configure(VMHypervisor.class, VMHypervisor.DEFAULT); @@ -112,6 +113,7 @@ public void loadConfig(@NonNull VMConfig config) { swUsb.setChecked(item.optBoolean("usb", false)); swSandbox.setChecked(item.optBoolean("sandbox", false)); swHugepages.setChecked(item.optBoolean("hugepages", false)); + swDebug.setChecked(item.optBoolean("strace", false)); choosePrepareLendMthp.setSelectedItem(LendMthpMode.fromItem(item)); chooseProtectedVm.setSelectedItem(optEnum(item, "protected_vm", PROTECTED_WITHOUT_FIRMWARE)); chooseBackend.setSelectedItem(optEnum(item, "backend", VMBackend.DEFAULT)); @@ -206,6 +208,7 @@ public void saveConfig(@NonNull VMConfig config) { item.set("usb", swUsb.isChecked()); item.set("sandbox", swSandbox.isChecked()); item.set("hugepages", swHugepages.isChecked()); + item.set("strace", swDebug.isChecked()); LendMthpMode lendMthpMode = choosePrepareLendMthp.getSelectedItem(); item.set(LendMthpMode.KEY, lendMthpMode); ProtectedVM pvm = chooseProtectedVm.getSelectedItem(); diff --git a/app/src/main/res/layout/partial_vm_edit_basic.xml b/app/src/main/res/layout/partial_vm_edit_basic.xml index 7faec6450..4489f872f 100644 --- a/app/src/main/res/layout/partial_vm_edit_basic.xml +++ b/app/src/main/res/layout/partial_vm_edit_basic.xml @@ -142,6 +142,14 @@ android:icon="@drawable/ic_usb" android:text="@string/create_vm_usb" /> + + RNG (随机数生成器) SMT (同步多线程) USB (通用串行总线) + 调试追踪 保护模式 SWIOTLB 大小(保护模式) 沙箱 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index ac7ed8b6b..46864f453 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -176,6 +176,7 @@ RNG (隨機數生成器) SMT (同步多執行緒) USB (通用序列匯流排) + 除錯追蹤 保護模式 SWIOTLB 大小(保護模式) 沙箱 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d14f63995..632d40281 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -179,6 +179,7 @@ RNG (Random Number Generator) SMT (Simultaneous MultiThread) USB (Universal Serial Bus) + Debug trace Protected VM SWIOTLB Size (Protected VM) Sandbox From 671b3f49b0d59ed5382f7bfe9b7e981f8d961c5d Mon Sep 17 00:00:00 2001 From: BigfootACA Date: Sat, 27 Jun 2026 11:04:32 +0800 Subject: [PATCH 056/182] assets: update prebuilts https://github.com/Droid-VM/DroidVM-Prebuilt-Root/commit/abccf596613f41c475376a7ed29c9d29b1dac559 Signed-off-by: BigfootACA --- app/src/main/assets/prebuilts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/assets/prebuilts b/app/src/main/assets/prebuilts index 56a9a9955..a6a039afa 160000 --- a/app/src/main/assets/prebuilts +++ b/app/src/main/assets/prebuilts @@ -1 +1 @@ -Subproject commit 56a9a99551ad70293f33b4c45c6ebb48ecd08074 +Subproject commit a6a039afafe28b8b835570f60fd63bcc6218d753 From 36150b5a1e23c4f1d3b4f5982be66059ed5cba59 Mon Sep 17 00:00:00 2001 From: BigfootACA Date: Sat, 27 Jun 2026 10:46:49 +0800 Subject: [PATCH 057/182] strings.xml: add missing zh-rTW translations Signed-off-by: BigfootACA --- app/src/main/res/values-zh-rTW/strings.xml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 46864f453..7273b783a 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -132,6 +132,7 @@ 找不到任何控制台 文字控制台 %1$s VNC 控制台 + 原生顯示 外部顯示器的 VNC 日誌 @@ -182,6 +183,9 @@ 沙箱 大頁記憶體 預分配大頁 + 停用 + 單次 (整塊借出) + 分塊 (≤256MiB) 普通 保護 無韌體保護 @@ -210,6 +214,7 @@ 垂直DPI VNC 伺服器 啟用 VNC + 啟用原生顯示 (virtio-gpu) 主機地址 埠 密碼認證 @@ -799,6 +804,12 @@ 無法連線到 VNC 伺服器 全屏 + 原生顯示器 + 正在連接… + 正在等待虛擬機器畫面… + 已連接 - %1$dx%2$d + 原生顯示啟動失敗 + 軟鍵盤 額外按鍵 全屏 From 5c0149c06982b9d2b9b0ac0e7bda93166a6a502b Mon Sep 17 00:00:00 2001 From: BigfootACA Date: Sat, 27 Jun 2026 10:55:48 +0800 Subject: [PATCH 058/182] strings.xml: fix ASCII compatible in default strings.xml Signed-off-by: BigfootACA --- app/src/main/res/values/strings.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 632d40281..abf3e6087 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -187,7 +187,7 @@ Prepare Lend mTHP Disabled Single (whole region) - Chunked (≤256MiB) + Chunked (≤256MiB) Normal Protected Protected without firmware @@ -842,8 +842,8 @@ Fullscreen Native Display - Connecting… - Waiting for VM display… + Connecting... + Waiting for VM display... Connected - %1$dx%2$d Failed to start native display From 8093cd0d609afc4a5829b8edfb88a84c00d6ba70 Mon Sep 17 00:00:00 2001 From: BigfootACA Date: Sat, 27 Jun 2026 14:36:15 +0800 Subject: [PATCH 059/182] VMEditBasicTab: disable debug trace by default Signed-off-by: BigfootACA --- .../cn/classfun/droidvm/ui/vm/edit/basic/VMEditBasicTab.java | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/basic/VMEditBasicTab.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/basic/VMEditBasicTab.java index 80c0609fe..f6d8eeebe 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/basic/VMEditBasicTab.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/basic/VMEditBasicTab.java @@ -81,6 +81,7 @@ public void initValue() { inputMemory.setValue(512, SizeUnit.MB); inputCpu.setValue(1); inputSwiotlb.setValue(64, SizeUnit.MB); + swDebug.setChecked(false); chooseProtectedVm.configure(ProtectedVM.class, PROTECTED_WITHOUT_FIRMWARE); chooseBackend.configure(VMBackend.class, VMBackend.DEFAULT); chooseHypervisor.configure(VMHypervisor.class, VMHypervisor.DEFAULT); From 638961dd293b8257e7cc96748b694e759d8bae9e Mon Sep 17 00:00:00 2001 From: BigfootACA Date: Sat, 27 Jun 2026 14:36:54 +0800 Subject: [PATCH 060/182] VirtualizationStepFragment: remove com.android.virt from failed string Signed-off-by: BigfootACA --- .../droidvm/ui/setup/step/VirtualizationStepFragment.java | 2 +- app/src/main/res/values-zh-rCN/strings.xml | 2 +- app/src/main/res/values-zh-rTW/strings.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/cn/classfun/droidvm/ui/setup/step/VirtualizationStepFragment.java b/app/src/main/java/cn/classfun/droidvm/ui/setup/step/VirtualizationStepFragment.java index 14851c791..8a8aee400 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/setup/step/VirtualizationStepFragment.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/setup/step/VirtualizationStepFragment.java @@ -67,10 +67,10 @@ private void operationThread() { if (!isAdded()) return; if (finalSuccess) { showSuccess(R.string.setup_virt_title, R.string.setup_virt_success); - showDetail(displayString.toString()); } else { showError(R.string.setup_virt_title, R.string.setup_virt_fail); } + showDetail(displayString.toString()); }); } diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 6f0667c0d..9467c7d6d 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -42,7 +42,7 @@ 检查虚拟化支持 正在检查虚拟化支持… \u2705 已找到虚拟化支持。 - \u274C 未找到虚拟化支持,请确认已安装 com.android.virt APEX。 + \u274C 未找到虚拟化支持。 文件访问权限 正在检查文件访问权限… diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 7273b783a..1804c2046 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -42,7 +42,7 @@ 檢查虛擬化支援 正在檢查虛擬化支援… \u2705 已找到虛擬化支援。 - \u274C 未找到虛擬化支援,請確認已安裝 com.android.virt APEX。 + \u274C 未找到虛擬化支援。 檔案存取權限 正在檢查檔案存取權限… diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index abf3e6087..3d6f816ad 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -43,7 +43,7 @@ Check Virtualization Checking virtualization support… \u2705 Virtualization support found. - \u274C Virtualization support not found. Please ensure the com.android.virt APEX is available. + \u274C Virtualization support not found. File Access Checking file access permission… From cdfed1c50bea9a3caf59e18d5799144dd38b4cf9 Mon Sep 17 00:00:00 2001 From: BigfootACA Date: Sat, 27 Jun 2026 15:22:18 +0800 Subject: [PATCH 061/182] README.md: add GenieZone and KVM Signed-off-by: BigfootACA --- README.md | 15 ++++++++++++--- README.zh.md | 16 ++++++++++++---- app/src/main/res/values-zh-rCN/strings.xml | 10 +++++----- app/src/main/res/values-zh-rTW/strings.xml | 10 +++++----- app/src/main/res/values/strings.xml | 10 +++++----- 5 files changed, 39 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index f8fe90236..957ed79f7 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,13 @@ [简体中文](README.zh.md) | [English](README.md) -A virtual machine manager for Android, powered by the **Qualcomm Gunyah Hypervisor** +An Android virtual machine manager with support for multiple hypervisors, including Qualcomm Gunyah, MediaTek GenieZone, and Linux KVM. + +Supported hypervisors: + +- Qualcomm Gunyah (`/dev/gunyah`) +- MediaTek GenieZone (`/dev/gzvm`) +- Linux KVM (`/dev/kvm`) Create and manage lightweight VMs directly on your device with near-native performance. @@ -34,8 +40,11 @@ Create and manage lightweight VMs directly on your device with near-native perfo - **Android 13** (API 33) or newer - **Root access** (Magisk, KernelSU, APatch, or similar) -- **Qualcomm Snapdragon 8 Gen 3** (SM8650) or newer SoC, **Qualcomm Snapdragon 8 Elite** (SM8750) recommended, with Gunyah Hypervisor enabled -- **Virtualization** enabled (/dev/gunyah or /dev/kvm) +- A supported ARM64 device with hardware virtualization enabled by firmware and kernel: + - Qualcomm: **Snapdragon 8 Gen 3** (SM8650) or newer SoC, **Snapdragon 8 Elite** (SM8750) recommended, with Gunyah enabled + - MediaTek: **Dimensity 9000** or newer SoC with GenieZone enabled + - Other ARM64 devices: booted with EL2 and Linux KVM enabled +- An available virtualization device node: `/dev/gunyah`, `/dev/gzvm`, or `/dev/kvm` ## Building diff --git a/README.zh.md b/README.zh.md index d98bc93c4..222ef4706 100644 --- a/README.zh.md +++ b/README.zh.md @@ -4,7 +4,13 @@ [简体中文](README.zh.md) | [English](README.md) -基于 **高通 Gunyah Hypervisor** 的 Android 虚拟机管理器 +支持多种 Hypervisor 的 Android 虚拟机管理器,包括高通 Gunyah、联发科 GenieZone 和 Linux KVM。 + +支持的虚拟化方案: + +- 高通 Gunyah(`/dev/gunyah`) +- 联发科 GenieZone(`/dev/gzvm`) +- Linux KVM(`/dev/kvm`) 在安卓设备上直接创建和管理轻量级虚拟机,享受接近原生的性能。 @@ -34,8 +40,11 @@ - **Android 13**(API 33)或更高版本 - **Root 权限**(Magisk、KernelSU、APatch 或类似工具) -- **高通骁龙 8 Gen 3**(SM8650)或更新的 SoC,推荐 **高通骁龙 8 Elite**(SM8750),需启用 Gunyah Hypervisor -- 已启用 **虚拟化** 支持(/dev/gunyah 或 /dev/kvm) +- 固件和内核已启用硬件虚拟化的 ARM64 设备: + - 高通:**骁龙 8 Gen 3**(SM8650)或更新 SoC,推荐 **骁龙 8 Elite**(SM8750),需启用 Gunyah + - 联发科:**天玑 9000** 或更新 SoC,需启用 GenieZone + - 其他 ARM64 设备:以 EL2 启动,并启用 Linux KVM +- 可用的虚拟化设备节点:`/dev/gunyah`、`/dev/gzvm` 或 `/dev/kvm` ## 构建 @@ -59,4 +68,3 @@ git submodule update --init --recursive ## 许可证 本项目基于 **GNU 通用公共许可证 v3.0** 发布。详见 [LICENSE.txt](LICENSE.txt)。 - diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 9467c7d6d..4707a9ea7 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -2,17 +2,17 @@ DroidVM - 基于高通 Gunyah 虚拟机监视器的虚拟机管理器,可在设备上直接创建和管理轻量级 Linux 虚拟机。 + 支持高通 Gunyah、联发科 GenieZone 和 Linux KVM 的 Android 虚拟机管理器,可在设备上直接创建和管理轻量级 Linux 或 Windows 虚拟机。 常见问题 - 什么是 Gunyah? - Gunyah 是高通开发的 Type-1 虚拟机监视器,直接运行在 Android 宿主系统的硬件层之下。它可以创建具有接近原生性能和强安全保障的隔离虚拟机,无需依赖内核级虚拟化(KVM)。 + 什么是虚拟化? + Gunyah:高通开发的 Type-1 虚拟机监视器,提供受保护的虚拟机支持。\nGenieZone:联发科开发的 Type-1 虚拟机监视器,提供硬件辅助虚拟机支持。\nLinux KVM:Linux 内核内置的虚拟化框架,适用于以 EL2 启动且启用虚拟化扩展的 ARM64 设备。 支持哪些设备? - 目前仅支持搭载高通骁龙 8 Elite(SM8750)或更新 SoC 且已启用 Gunyah 支持的设备。旧款骁龙芯片组不包含必要的虚拟机监视器固件。 + 高通:骁龙 8 Gen 3(SM8650)或更新 SoC 且已启用 Gunyah 的设备,推荐骁龙 8 Elite(SM8750)或更新平台。\n联发科:天玑 9000 或更新 SoC 且已启用 GenieZone 的设备。\nLinux KVM:以 EL2 启动的 ARM64 设备,例如 Google Tensor、瑞芯微等平台。 有哪些环境要求? - 1. Root 权限(如 Magisk 或 KernelSU)。\n2. 必须安装虚拟化 APEX(com.android.virt),其中提供了 crosvm 二进制文件。\n3. 支持的高通 SoC,且固件中已启用 Gunyah 虚拟机监视器。 + 1. Root 权限(如 Magisk、KernelSU 或 APatch)。\n2. 支持的 ARM64 SoC,且固件和内核已启用 Gunyah、GenieZone 或 Linux KVM。\n3. 可用的虚拟化设备节点:/dev/gunyah、/dev/gzvm 或 /dev/kvm。 功能未实现 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 1804c2046..9f5ba258f 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -2,17 +2,17 @@ DroidVM - 基於高通 Gunyah 虛擬機器監視器的虛擬機器管理器,可在裝置上直接建立和管理輕量級 Linux 虛擬機器。 + 支援高通 Gunyah、聯發科 GenieZone 和 Linux KVM 的 Android 虛擬機器管理器,可在裝置上直接建立和管理輕量級 Linux 或 Windows 虛擬機器。 常見問題 - 什麼是 Gunyah? - Gunyah 是高通開發的 Type-1 虛擬機器監視器,直接執行在 Android 宿主系統的硬體層之下。它可以建立具有接近原生效能和強安全保障的隔離虛擬機器,無需依賴核心級虛擬化(KVM)。 + 什麼是虛擬化? + Gunyah:高通開發的 Type-1 虛擬機器監視器,提供受保護的虛擬機器支援。\nGenieZone:聯發科開發的 Type-1 虛擬機器監視器,提供硬體輔助虛擬機器支援。\nLinux KVM:Linux 核心內建的虛擬化框架,適用於以 EL2 啟動且啟用虛擬化擴充的 ARM64 裝置。 支援哪些裝置? - 目前僅支援搭載高通驍龍 8 Elite(SM8750)或更新 SoC 且已啟用 Gunyah 支援的裝置。舊款驍龍晶片組不包含必要的虛擬機器監視器韌體。 + 高通:驍龍 8 Gen 3(SM8650)或更新 SoC 且已啟用 Gunyah 的裝置,建議使用驍龍 8 Elite(SM8750)或更新平台。\n聯發科:天璣 9000 或更新 SoC 且已啟用 GenieZone 的裝置。\nLinux KVM:以 EL2 啟動的 ARM64 裝置,例如 Google Tensor、瑞芯微等平台。 有哪些環境要求? - 1. Root 權限(如 Magisk 或 KernelSU)。\n2. 必須安裝虛擬化 APEX(com.android.virt),其中提供了 crosvm 二進位檔案。\n3. 支援的高通 SoC,且韌體中已啟用 Gunyah 虛擬機器監視器。 + 1. Root 權限(如 Magisk、KernelSU 或 APatch)。\n2. 支援的 ARM64 SoC,且韌體和核心已啟用 Gunyah、GenieZone 或 Linux KVM。\n3. 可用的虛擬化裝置節點:/dev/gunyah、/dev/gzvm 或 /dev/kvm。 功能未實現 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3d6f816ad..9dc1642ca 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3,17 +3,17 @@ DroidVM - A virtual machine manager powered by Qualcomm Gunyah Hypervisor. Create and manage lightweight Linux VMs directly on your device. + A virtual machine manager for Android with support for Qualcomm Gunyah, MediaTek GenieZone, and Linux KVM. Create and manage lightweight Linux or Windows VMs directly on your device. Frequently Asked Questions - What is Gunyah? - Gunyah is a Type-1 hypervisor developed by Qualcomm that runs directly on the hardware beneath the Android host OS. It allows creating isolated virtual machines with near-native performance and strong security guarantees, without relying on kernel-level virtualization (KVM). + What is virtualization? + Gunyah: a Type-1 hypervisor developed by Qualcomm for protected virtual machines.\nGenieZone: a Type-1 hypervisor developed by MediaTek for hardware-assisted virtual machines.\nLinux KVM: the Linux kernel virtualization framework, available on ARM64 devices booted with EL2 and virtualization extensions enabled. Which devices are supported? - Currently only devices with Qualcomm Snapdragon 8 Elite (SM8750) or newer SoCs that ship with Gunyah support enabled are supported. Older Snapdragon chipsets do not include the necessary hypervisor firmware. + Qualcomm: Snapdragon 8 Gen 3 (SM8650) or newer SoCs with Gunyah enabled. Snapdragon 8 Elite (SM8750) or newer is recommended.\nMediaTek: Dimensity 9000 or newer SoCs with GenieZone enabled.\nLinux KVM: ARM64 devices booted with EL2, such as Google Tensor and Rockchip devices. What are the environment requirements? - 1. Root access (e.g. Magisk or KernelSU).\n2. The virtualization APEX (com.android.virt) must be installed, which provides the crosvm binary.\n3. A supported Qualcomm SoC with Gunyah hypervisor enabled in firmware. + 1. Root access (for example, Magisk, KernelSU, or APatch).\n2. A supported ARM64 SoC with Gunyah, GenieZone, or Linux KVM enabled by firmware and kernel.\n3. An available virtualization device node: /dev/gunyah, /dev/gzvm, or /dev/kvm. Function not implement From b5e2cade221d8f3dd3a72aca1b8c7c6146cbed3c Mon Sep 17 00:00:00 2001 From: BigfootACA Date: Sat, 27 Jun 2026 15:22:51 +0800 Subject: [PATCH 062/182] MainHomeFragment: set document url to github pages Signed-off-by: BigfootACA --- app/src/main/java/cn/classfun/droidvm/lib/Constants.java | 1 - .../cn/classfun/droidvm/ui/main/home/MainHomeFragment.java | 3 +-- app/src/main/res/values-zh-rCN/strings.xml | 1 + app/src/main/res/values-zh-rTW/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 5 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/cn/classfun/droidvm/lib/Constants.java b/app/src/main/java/cn/classfun/droidvm/lib/Constants.java index f15802dd9..9256ea2c1 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/Constants.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/Constants.java @@ -18,7 +18,6 @@ public final class Constants { public static final String PATH_BUILTIN_INITRD = pathJoin(DATA_DIR, "/usr/share/droidvm/initramfs.img"); public static final String PATH_EDK2_FIRMWARE = pathJoin(DATA_DIR, "/usr/share/droidvm/edk2-gunyah.fd"); public static final String PATH_EDK2_QEMU_FIRMWARE = pathJoin(DATA_DIR, "/usr/share/droidvm/edk2-qemu.fd"); - public static final String GITHUB_WIKI_URL = "https://github.com/Droid-VM/DroidVM/wiki"; public static final String GITHUB_ISSUE_URL = "https://github.com/Droid-VM/DroidVM/issues"; // droidvm + daemon are CMake-built and shipped as APK assets. The // third-party daemons (gvswitch/pbridge/bridgedhcp/netbox) and lbx now diff --git a/app/src/main/java/cn/classfun/droidvm/ui/main/home/MainHomeFragment.java b/app/src/main/java/cn/classfun/droidvm/ui/main/home/MainHomeFragment.java index fe78d4fd4..47e1261df 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/main/home/MainHomeFragment.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/main/home/MainHomeFragment.java @@ -3,7 +3,6 @@ import static android.content.Intent.ACTION_VIEW; import static android.widget.Toast.LENGTH_SHORT; import static cn.classfun.droidvm.lib.Constants.GITHUB_ISSUE_URL; -import static cn.classfun.droidvm.lib.Constants.GITHUB_WIKI_URL; import static cn.classfun.droidvm.lib.size.SizeUtils.formatSize; import static cn.classfun.droidvm.lib.utils.ThreadUtils.runOnPool; @@ -113,7 +112,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat view.findViewById(R.id.card_network).setOnClickListener(v -> navigateToTab(R.id.nav_network)); view.findViewById(R.id.card_wizard).setOnClickListener(v -> openWizard()); view.findViewById(R.id.card_import).setOnClickListener(v -> openImport()); - view.findViewById(R.id.card_help).setOnClickListener(v -> openLink(GITHUB_WIKI_URL)); + view.findViewById(R.id.card_help).setOnClickListener(v -> openLink(getString(R.string.home_card_help_url))); view.findViewById(R.id.card_feedback).setOnClickListener(v -> openLink(GITHUB_ISSUE_URL)); cardUpdate.setOnClickListener(v -> showUpdateDialog()); checkForUpdate(); diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 4707a9ea7..57eaa15ee 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -86,6 +86,7 @@ 帮助与文档 反馈问题 加载中\u2026 + https://droid-vm.github.io/ 暂无虚拟机。 暂无磁盘。 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 9f5ba258f..8ff732ffe 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -86,6 +86,7 @@ 幫助與文件 反饋問題 載入中\u2026 + https://droid-vm.github.io/ 暫無虛擬機器。 暫無磁碟。 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9dc1642ca..e6b5d8c90 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -87,6 +87,7 @@ Help & Documentation Report Issue Loading\u2026 + https://droid-vm.github.io/en/ No virtual machines yet. No disks yet. From 3c32f7d8062394f45e0cacf1889b4f4c9325d86b Mon Sep 17 00:00:00 2001 From: HuJK Date: Sat, 27 Jun 2026 21:43:05 +0800 Subject: [PATCH 063/182] (fix) add lock=false on scsi drive --- .../droidvm/daemon/vm/backend/CrosvmBackendInstance.java | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackendInstance.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackendInstance.java index e66114e38..7a4e9a566 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackendInstance.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackendInstance.java @@ -241,6 +241,7 @@ private void buildDiskCommand(@NonNull List args) { var arg = new StringBuilder(path); switch (bus) { case SCSI: + arg.append(",lock=false"); if (readonly) arg.append(",ro=true"); args.add("--scsi-block"); args.add(arg.toString()); From bde5d51505508d8bb522370a87541e1c9c493006 Mon Sep 17 00:00:00 2001 From: HuJK Date: Sat, 27 Jun 2026 21:58:24 +0800 Subject: [PATCH 064/182] build: skip prebuilts submodule checkout in local build flow When DroidVM-Prebuilt-Root is active the prebuilts are regenerated by auto-build.py, so checking out the DroidVM-Prebuilts submodule only clobbers the locally-built artifacts and moves the recorded pointer. Update every other submodule instead. --- build.sh | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/build.sh b/build.sh index 21648066e..8b99be568 100755 --- a/build.sh +++ b/build.sh @@ -66,8 +66,19 @@ fi # ---------------------------------------------------------------- submodules # Includes the DroidVM-Prebuilts submodule (prebuilt artifacts) and the CMake # submodules (libvncserver, jsoncpp, udisasm). +PREBUILT_SUBMODULE="app/src/main/assets/prebuilts" log "Initializing git submodules" -git submodule update --init --recursive +if prebuilt_root_active; then + # Local build flow: prebuilts are regenerated by auto-build.py, so checking + # out the DroidVM-Prebuilts submodule would only clobber the locally-built + # artifacts (and move the recorded pointer). Update every other submodule. + log "Local prebuilt build active; skipping $PREBUILT_SUBMODULE checkout" + paths=$(git config -f .gitmodules --get-regexp '^submodule\..*\.path$' \ + | awk '{print $2}' | grep -vx "$PREBUILT_SUBMODULE" || true) + [ -n "$paths" ] && git submodule update --init --recursive -- $paths +else + git submodule update --init --recursive +fi # --------------------------------------------------------------------- APK log "Building APK ($GRADLE_TASK)" From c3e8ae2e21ba4901eb8db2798edcdbf62e6675e9 Mon Sep 17 00:00:00 2001 From: BigfootACA Date: Sat, 27 Jun 2026 22:59:05 +0800 Subject: [PATCH 065/182] DiskActionDialog: fix crash on target disk already exists Signed-off-by: BigfootACA --- .../ui/disk/action/DiskActionDialog.java | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/cn/classfun/droidvm/ui/disk/action/DiskActionDialog.java b/app/src/main/java/cn/classfun/droidvm/ui/disk/action/DiskActionDialog.java index bbbe022bf..56850ee19 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/disk/action/DiskActionDialog.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/disk/action/DiskActionDialog.java @@ -228,14 +228,33 @@ public DiskConfig onFileImported(Uri uri) { config.item.set("folder", folder); var store = new DiskStore(); runOnPool(() -> { - store.load(context); - store.add(config); - store.save(context); + try { + store.load(context); + if (store.findByName(fileName) != null) { + mainLooper.post(() -> Toast.makeText( + context, + R.string.disk_create_error_exists, + LENGTH_SHORT + ).show()); + return; + } + store.add(config); + store.save(context); + } catch (Exception ignored) { + mainLooper.post(() -> Toast.makeText( + context, + R.string.disk_create_error_folder_invalid, + LENGTH_SHORT + ).show()); + return; + } + mainLooper.post(() -> { + var str = context.getString(R.string.disk_create_success, fileName); + Toast.makeText(context, str, LENGTH_SHORT).show(); + }); if (this.onUpdate != null) this.onUpdate.run(); }); - var str = context.getString(R.string.disk_create_success, fileName); - Toast.makeText(context, str, LENGTH_SHORT).show(); return config; } From 9c733072995fc9ccc78ccd7610028bf499b41aed Mon Sep 17 00:00:00 2001 From: BigfootACA Date: Sat, 27 Jun 2026 22:59:30 +0800 Subject: [PATCH 066/182] StringUtils: refactor resolveUriPath for more uri document path parse Signed-off-by: BigfootACA --- .../droidvm/lib/utils/StringUtils.java | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/cn/classfun/droidvm/lib/utils/StringUtils.java b/app/src/main/java/cn/classfun/droidvm/lib/utils/StringUtils.java index 58000a3f6..879cffdc0 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/utils/StringUtils.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/utils/StringUtils.java @@ -61,8 +61,22 @@ private static String getRealPathFromURI(@NonNull Context context, Uri uri) { return null; } - public static String resolveUriPath(Context ctx, Uri uri) { + @Nullable + private static String parseTreePath(@Nullable String content, @Nullable String tag) { var extStorage = externalPath(); + if (content == null) return null; + if (tag != null) { + if (!content.startsWith(tag)) return null; + content = content.substring(tag.length()); + } + if (shellCheckExists(content)) return content; + var real = pathJoin(extStorage, content); + if (shellCheckExists(real)) return real; + return null; + } + + public static String resolveUriPath(Context ctx, Uri uri) { + String part; try { var real = getRealPathFromURI(ctx, uri); if (real != null && shellCheckExists(real)) return real; @@ -70,26 +84,23 @@ public static String resolveUriPath(Context ctx, Uri uri) { } try { var docId = DocumentsContract.getDocumentId(uri); - if (docId != null && docId.startsWith("primary:")) - return pathJoin(extStorage, docId.substring("primary:".length())); + if ((part = parseTreePath(docId, "primary:")) != null) return part; } catch (Exception ignored) { } try { var treeId = DocumentsContract.getTreeDocumentId(uri); - if (treeId != null && treeId.startsWith("primary:")) - return pathJoin(extStorage, treeId.substring("primary:".length())); + if ((part = parseTreePath(treeId, "primary:")) != null) return part; } catch (Exception ignored) { } var path = uri.getPath(); - if (path != null && path.startsWith("/document/primary:")) - return pathJoin(extStorage, path.substring("/document/primary:".length())); - if (path != null && path.startsWith("/external_files/")) - return pathJoin(extStorage, path.substring("/external_files/".length())); + if ((part = parseTreePath(path, "/document/primary:")) != null) return part; + if ((part = parseTreePath(path, "/external_files/")) != null) return part; + if ((part = parseTreePath(path, "/tree/primary:")) != null) return part; if (path != null && path.startsWith("/tree/primary:")) { var sub = path.substring("/tree/primary:".length()); int slash = sub.indexOf("/document/"); if (slash >= 0) sub = sub.substring(0, slash); - return pathJoin(extStorage, sub); + return pathJoin(externalPath(), sub); } return path; } From 11b33301bde11d4aa50d4f74aac23c899ce2080d Mon Sep 17 00:00:00 2001 From: HuJK Date: Mon, 29 Jun 2026 14:49:02 +0800 Subject: [PATCH 067/182] network: route Linux-bridge guests through the VPN like local apps Forwarded guest packets carry no fwmark, so they never match netd's uid/netId-keyed "iif lo" rules and fall through to the default network -- breaking guest access to VPN-backed services and the reverse path of port forwards from VPN clients. DefaultRouterWatcher now mirrors the tables a default app traverses (the secure-VPN and default-network selection rules, identified by fwmark 0 + fwmask present + iif lo) into per-bridge "iif
lookup " rules, in netd priority order so a split tunnel's VPN table is consulted before the default one. Ordering is pinned with explicit FRA_PRIORITY (RULE_PRIORITY_BASE + i) rather than insertion order, which the kernel matches LIFO on-device and silently reversed (also caused per-tick churn). - netbox: add --priority to rule-add/rule-del so the chain order is deterministic across kernel versions - IptablesBackend: allow FORWARD to/from tun+ so guests routed to the VPN table are forwarded out tun0 instead of hitting the trailing DROP - prebuilts: bump to 06a1cc4 (netbox with --priority) Co-Authored-By: Claude Opus 4.8 --- app/src/main/assets/prebuilts | 2 +- .../network/backend/DefaultRouterWatcher.java | 239 ++++++++++++------ .../daemon/network/backend/Netbox.java | 10 +- .../backend/iptables/IptablesBackend.java | 7 +- 4 files changed, 181 insertions(+), 77 deletions(-) diff --git a/app/src/main/assets/prebuilts b/app/src/main/assets/prebuilts index a6a039afa..06a1cc4e4 160000 --- a/app/src/main/assets/prebuilts +++ b/app/src/main/assets/prebuilts @@ -1 +1 @@ -Subproject commit a6a039afafe28b8b835570f60fd63bcc6218d753 +Subproject commit 06a1cc4e47be68d030aa54546175c8172f2786a0 diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/DefaultRouterWatcher.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/DefaultRouterWatcher.java index 7853a0257..b17f26bec 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/DefaultRouterWatcher.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/DefaultRouterWatcher.java @@ -5,12 +5,13 @@ import android.util.Log; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; -import java.util.Objects; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executors; @@ -27,6 +28,15 @@ public final class DefaultRouterWatcher { private static final String TAG = "DefaultRouterWatcher"; private static final long POLL_INTERVAL_SEC = 5; + /** + * FRA_PRIORITY of the first "iif {bridge} lookup {table}" rule; rule i in a + * bridge's chain gets {@code RULE_PRIORITY_BASE + i}. Sits above the local + * table (priority 0) and below netd's lowest rule (10000), so a guest packet + * is matched by the local table first, then our chain, before netd's + * "from all" fallbacks. Leaves headroom for far more tables than any device + * has networks. + */ + private static final int RULE_PRIORITY_BASE = 9000; /** * Interface-name prefixes whose IPv4 addresses count as the phone's own * reachable IPs for port-forward DNAT scoping: Wi-Fi, cellular, VPN, @@ -43,8 +53,8 @@ public final class DefaultRouterWatcher { private final List listeners = new CopyOnWriteArrayList<>(); private final List hostIpListeners = new CopyOnWriteArrayList<>(); private volatile ScheduledExecutorService scheduler; - private String lastDefault4 = null; - private String lastDefault6 = null; + private List lastTables4 = List.of(); + private List lastTables6 = List.of(); /** Phone's own IPv4 addresses; the netbox host-ips policy decides the set. */ private volatile Set hostIpv4 = Set.of(); /** netbox monitor-addr: pushes host-IP changes the instant they happen. */ @@ -57,97 +67,185 @@ public DefaultRouterWatcher(@NonNull ServerContext context) { } /** - * The routing table of the current default network, per family: Android - * keeps the default route out of main and selects it with the netd rule - * "from all fwmark 0x0/0xffff iif lo lookup {table}". + * The ordered routing tables a default-network app's locally-generated + * traffic would traverse, per family -- the egress half of guest routing. + * Forwarded guest packets carry no fwmark, so they never match netd's + * uid/netId-keyed "iif lo" rules; we mirror the tables those rules select + * into "iif {bridge}" rules instead (see {@link #reconcileRules}). + * + *

A default app's packet has fwmark 0 (no explicitly selected network, + * not VPN-protected), so it is selected by exactly the "iif lo" lookup + * rules whose fwmark is unset/0: + *

    + *
  • the secure-VPN rule ("from all fwmark 0x0/0x20000 iif lo ... lookup + * {vpn}", priority 13000) -> the VPN table holding a split tunnel's + * routes (e.g. 10.0.0.0/8);
  • + *
  • the default-network rule ("from all fwmark 0x0/0xffff iif lo lookup + * {default}") -> the underlying network's table (e.g. wlan0).
  • + *
+ * netId explicit/implicit rules carry a nonzero fwmark and are skipped. + * Returned in netd priority order so a split-tunnel VPN's table is consulted + * before the default one: a guest packet to 10.x matches the VPN table; + * anything else finds no route there and falls through to the default table + * (kernel policy-rule fall-through). With no VPN the list is just the single + * default table; a full-tunnel VPN's table has a default route, so the trailing + * default table is simply never reached. + * + *

Matching is on what netbox exposes (priority, iif, table, fwmark, + * fwmask -- not oif/uidrange), so the filter must single out the fwmark + * selection rules: + *

    + *
  • {@code lookup} -- it routes to a table (not unreachable/blackhole);
  • + *
  • {@code iif == "lo"} -- locally-generated selection rules, not the + * "from all" legacy/local-network rules that forwarded packets already + * traverse;
  • + *
  • {@code fwmark} unset/0 -- a default app's mark (the kernel omits + * FRA_FWMARK when the mark is 0); netId explicit/implicit rules carry a + * nonzero mark and are skipped;
  • + *
  • {@code fwmask} present -- this is the key discriminator. A real + * fwmark selector always emits FRA_FWMASK (the kernel sends it whenever + * the mask is nonzero, e.g. 0x20000/0xffff), even when the mark value + * is 0. The "iif lo oif <iface> ... lookup" output-interface rules + * (priorities 11000/17000, incl. dummy0 blackholes) carry no mark or + * mask, so their fwmask is null and they are excluded -- without this + * check those oif rules would be picked up (their oif is invisible to + * us) and wrongly rank wlan0/dummy0 ahead of the VPN table.
  • + *
*/ - @Nullable - private String currentDefaultTable(boolean ipv6) { - // netd selects the default network with "from all fwmark 0x0/0xffff - // iif lo lookup {table}" (mark 0, mask 0xffff). The kernel omits - // FRA_FWMARK when the mark is 0, so match on the 0xffff mask alone. + @NonNull + private List currentLocalAppTables(boolean ipv6) { + // ruleList is in kernel order = ascending priority, so collecting in + // iteration order preserves netd's precedence (secure-VPN before default). + var tables = new LinkedHashSet(); var rows = Netbox.ruleList(ipv6); for (int i = 0; i < rows.length(); i++) { var r = rows.optJSONObject(i); if (r == null) continue; - if (r.optInt("fwmark", 0) != 0) continue; - if (r.optInt("fwmask", 0) != 0xffff) continue; - return String.valueOf(r.optInt("table", 0)); + if (!r.optBoolean("lookup", false)) continue; + if (!"lo".equals(r.optString("iif", ""))) continue; + if (r.isNull("fwmask")) continue; + if (!r.isNull("fwmark") && r.optInt("fwmark", 0) != 0) continue; + tables.add(String.valueOf(r.optInt("table", 0))); } - return null; + return new ArrayList<>(tables); } - private void ruleDel(boolean ipv6, @NonNull String dev, @NonNull String table) { - Netbox.ruleDel(dev, table, ipv6); + private void ruleDel(boolean ipv6, @NonNull String dev, @NonNull String table, int priority) { + Netbox.ruleDel(dev, table, priority, ipv6); } - private void ruleAdd(boolean ipv6, @NonNull String dev, @NonNull String table) { - Netbox.ruleAdd(dev, table, ipv6); + private void ruleAdd(boolean ipv6, @NonNull String dev, @NonNull String table, int priority) { + Netbox.ruleAdd(dev, table, priority, ipv6); } /** - * Reconciles one family's "iif {bridge} lookup {default-table}" rules - * (the egress half of guest routing: forwarded packets carry no fwmark - * and Android's netd rules end in "from all unreachable", so each - * bridge needs an explicit rule into the default network's table). - * Declarative per tick: deletes stale rules -- wrong table, duplicates - * (added twice across restarts) and [detached] leftovers from a - * recreated bridge -- and adds what is missing. Stale-rule deletion by - * spec may remove either twin first; the next tick converges. + * Reconciles one family's per-bridge "iif {bridge} priority {p} lookup + * {table}" rule chains against {@code tables} (the egress half of guest + * routing: forwarded packets carry no fwmark and Android's netd rules end + * in "from all unreachable", so each bridge needs explicit rules into the + * tables a local app would use). Each running Linux-L3 bridge gets the + * whole ordered {@code tables} chain so split-tunnel works: the VPN table + * is tried before the default one and the kernel falls through when it has + * no matching route -- see {@link #currentLocalAppTables}. + * + *

Order matters (a packet to 10.x must hit the VPN table before the + * default table's 0.0.0.0/0). We pin it with explicit FRA_PRIORITY rather + * than insertion order: rule i gets priority {@code RULE_PRIORITY_BASE + i} + * (ascending = tables[0] first). Insertion order is unreliable -- the kernel + * matches same-priority rules FIFO or LIFO depending on version (observed + * LIFO on-device, which silently reversed the chain). The band sits below + * netd's lowest rule (10000) and above the local table (0), so a guest + * packet hits local first, then our chain. + * + *

Declarative per chain: a bridge whose installed rules (each as a + * "priority table" spec, sorted by priority) exactly equal the desired + * chain is left untouched; any other bridge -- wrong tables/priorities + * (incl. the pre-priority builds' priority-0 rules), [detached] from a + * recreated bridge, or no longer desired -- has all its rules deleted (each + * by its own priority) and, if still desired, the full chain re-added. An + * empty {@code tables} (no default network, or stop()) desires no bridge, + * so every leftover rule is swept. */ - private void reconcileRules(boolean ipv6, @Nullable String table) { - var desired = new LinkedHashSet(); + private void reconcileRules(boolean ipv6, @NonNull List tables) { + var desiredDevs = new LinkedHashSet(); var bridges = new ArrayList(); context.getNetworks().forEach((uuid, inst) -> { var br = inst.getBridgeName(); if (br != null && !br.isEmpty()) bridges.add(br); // Only a running Linux-bridge L3 network has a real kernel bridge - // that forwarded guest traffic needs an "iif lookup - //

" rule for. gvisor is a userspace data path (gvswitch + - // AF_XDP) with no kernel device for its bridge name, so a rule on - // it would sit [detached] and flap every tick; a stopped network - // has no bridge either. Whitelist the one case that needs rules -- - // any other backend is excluded by default. Both still go into + // that forwarded guest traffic needs "iif lookup
" + // rules for. gvisor is a userspace data path (gvswitch + AF_XDP) + // with no kernel device for its bridge name, so a rule on it would + // sit [detached] and flap every tick; a stopped network has no + // bridge either. Whitelist the one case that needs rules -- any + // other backend is excluded by default. Both still go into // `bridges` above so their leftover rules are swept here. - if (table != null + if (!tables.isEmpty() && inst.getState() == NetworkState.RUNNING && inst.getBridgeType() == BridgeType.LINUX && inst.getUplinkMode() == UplinkMode.L3) - desired.addAll(inst.getL3Devices()); + desiredDevs.addAll(inst.getL3Devices()); }); - var satisfied = new LinkedHashSet(); + // Desired chain as "priority table" specs in ascending priority; the + // same list for every desired bridge. tables[0] (the VPN table) gets the + // lowest priority so it is evaluated first. + var desired = new ArrayList(); + for (int i = 0; i < tables.size(); i++) + desired.add((RULE_PRIORITY_BASE + i) + " " + tables.get(i)); + + // Group our installed rules per device as "priority table" specs. Ours + // are the plain rules: an iif owned by a bridge, a lookup action, no + // fwmark/mask (netd's are all marked or oif/uid scoped). Sort each + // device's specs by priority so the comparison is order-canonical. + var installed = new LinkedHashMap>(); + var detached = new HashSet(); var rows = Netbox.ruleList(ipv6); for (int i = 0; i < rows.length(); i++) { var r = rows.optJSONObject(i); if (r == null) continue; var dev = r.optString("iif", ""); - // only our "iif lookup
" rules: skip rules with no - // iif, and netd's marked rules (any fwmark/mask present) if (dev.isEmpty()) continue; + if (!r.optBoolean("lookup", false)) continue; if (!r.isNull("fwmark") || !r.isNull("fwmask")) continue; boolean ours = false; for (var br : bridges) if (dev.startsWith(br)) { ours = true; break; } if (!ours) continue; - var tbl = String.valueOf(r.optInt("table", 0)); - boolean stale = r.optBoolean("detached", false) - || !tbl.equals(table) - || !desired.contains(dev) - || satisfied.contains(dev); - if (stale) { - Log.i(TAG, fmt("Removing stale rule: iif %s lookup %s (v%d)", - dev, tbl, ipv6 ? 6 : 4)); - ruleDel(ipv6, dev, tbl); - } else { - satisfied.add(dev); - } + installed.computeIfAbsent(dev, k -> new ArrayList<>()) + .add(r.optInt("priority", 0) + " " + r.optInt("table", 0)); + if (r.optBoolean("detached", false)) detached.add(dev); } - if (table != null) for (var dev : desired) { - if (satisfied.contains(dev)) continue; - Log.i(TAG, fmt("Adding rule: iif %s lookup %s (v%d)", - dev, table, ipv6 ? 6 : 4)); - ruleAdd(ipv6, dev, table); + installed.values().forEach(specs -> specs.sort(Comparator.comparingInt( + s -> Integer.parseInt(s.substring(0, s.indexOf(' ')))))); + + // A device is already correct iff it is still desired, has no detached + // rule, and its installed specs equal the desired chain (same tables AND + // priorities). Everything else is torn down -- each rule deleted by its + // own priority -- and desired-but-incorrect devices are rebuilt below. + var correct = new HashSet(); + installed.forEach((dev, specs) -> { + if (desiredDevs.contains(dev) + && !detached.contains(dev) + && specs.equals(desired)) { + correct.add(dev); + return; + } + Log.i(TAG, fmt("Removing stale chain: iif %s [%s] (v%d)", + dev, specs, ipv6 ? 6 : 4)); + for (var spec : specs) { + int sp = spec.indexOf(' '); + ruleDel(ipv6, dev, spec.substring(sp + 1), + Integer.parseInt(spec.substring(0, sp))); + } + }); + + for (var dev : desiredDevs) { + if (correct.contains(dev)) continue; + Log.i(TAG, fmt("Adding chain: iif %s [%s] (v%d)", + dev, desired, ipv6 ? 6 : 4)); + for (int i = 0; i < tables.size(); i++) + ruleAdd(ipv6, dev, tables.get(i), RULE_PRIORITY_BASE + i); } } @@ -159,19 +257,18 @@ private synchronized void runOnce() { private void updateDefaultRouter() { try { - var new4 = currentDefaultTable(false); - var new6 = currentDefaultTable(true); + var new4 = currentLocalAppTables(false); + var new6 = currentLocalAppTables(true); reconcileRules(false, new4); reconcileRules(true, new6); - boolean changed = !Objects.equals(new4, lastDefault4) - || !Objects.equals(new6, lastDefault6); + boolean changed = !new4.equals(lastTables4) || !new6.equals(lastTables6); if (changed) - Log.i(TAG, fmt("Default network now v4=%s v6=%s", new4, new6)); - lastDefault4 = new4; - lastDefault6 = new6; + Log.i(TAG, fmt("Local-app route tables now v4=%s v6=%s", new4, new6)); + lastTables4 = new4; + lastTables6 = new6; // notify only on appearance/change, not on loss: a reconnect to - // the same network re-triggers because the loss nulled the state - if (changed && (new4 != null || new6 != null)) + // the same network re-triggers because the loss emptied the state + if (changed && (!new4.isEmpty() || !new6.isEmpty())) notifyChange(); } catch (Exception e) { Log.w(TAG, "Failed to update default router", e); @@ -272,8 +369,8 @@ public void removeListener(@NonNull Runnable listener) { /** Installs the new network's rules immediately (the tick would lag 5s). */ public synchronized void setForNewNetwork() { try { - reconcileRules(false, currentDefaultTable(false)); - reconcileRules(true, currentDefaultTable(true)); + reconcileRules(false, currentLocalAppTables(false)); + reconcileRules(true, currentLocalAppTables(true)); } catch (Exception e) { Log.w(TAG, "Failed to install rules for new network", e); } @@ -304,8 +401,8 @@ public synchronized void stop() { } // sweep every rule of ours, both families try { - reconcileRules(false, null); - reconcileRules(true, null); + reconcileRules(false, List.of()); + reconcileRules(true, List.of()); } catch (Exception e) { Log.w(TAG, "Failed to sweep rules on stop", e); } diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/Netbox.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/Netbox.java index 3660714ea..3d08b0783 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/Netbox.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/Netbox.java @@ -103,13 +103,15 @@ static boolean routeAdd(@NonNull String dev, @NonNull String dst, @NonNull Strin } @SuppressWarnings("UnusedReturnValue") - static boolean ruleAdd(@NonNull String iif, @NonNull String table, boolean v6) { - return run(v6Args("rule-add", v6, "--iif", iif, "--table", table)); + static boolean ruleAdd(@NonNull String iif, @NonNull String table, int priority, boolean v6) { + return run(v6Args("rule-add", v6, "--iif", iif, "--table", table, + "--priority", String.valueOf(priority))); } @SuppressWarnings("UnusedReturnValue") - static boolean ruleDel(@NonNull String iif, @NonNull String table, boolean v6) { - return run(v6Args("rule-del", v6, "--iif", iif, "--table", table)); + static boolean ruleDel(@NonNull String iif, @NonNull String table, int priority, boolean v6) { + return run(v6Args("rule-del", v6, "--iif", iif, "--table", table, + "--priority", String.valueOf(priority))); } static boolean fdbAdd(@NonNull String mac, @NonNull String dev) { diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/iptables/IptablesBackend.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/iptables/IptablesBackend.java index ce4062773..f98e19dec 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/iptables/IptablesBackend.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/iptables/IptablesBackend.java @@ -60,7 +60,12 @@ public final class IptablesBackend extends FirewallHelper { FIL6_OUT, FIL6_FWD, }; - private static final String[] EXT_IFACES = {"wlan+", "eth+", "rmnet_data+"}; + // Uplink interfaces guest traffic may be forwarded out of (and replies in + // from); anything else hits the trailing DROP. "tun+" covers the system VPN + // so a guest routed to the VPN table (see DefaultRouterWatcher) is actually + // forwarded out tun0 instead of dropped -- it mirrors the "tun" prefix in + // that watcher's HOST_IFACE_PREFIXES, which this list is meant to track. + private static final String[] EXT_IFACES = {"wlan+", "eth+", "rmnet+", "tun+"}; synchronized boolean iptables(@NonNull String... args) { int retry = 0; From 79af345fe67b2d6e0ee55fa2b2fa9d52d1ceacaa Mon Sep 17 00:00:00 2001 From: HuJK Date: Tue, 30 Jun 2026 09:50:34 +0800 Subject: [PATCH 068/182] vnc: use daemon-resolved LAN address for IPv4-wildcard binds When a VM binds VNC to 0.0.0.0, resolve the phone's own LAN IPv4 from the router watcher's filtered host-IP set (which already excludes pbridge offload-proxy addresses) and use it for the external/remote connect URI. The client cannot exclude those itself: the offload tag is a netlink route metric invisible to java.net.NetworkInterface. Co-Authored-By: Claude Opus 4.8 --- .../droidvm/daemon/ipc/vm/VncInfoHandler.java | 12 ++++++++++ .../vm/display/vnc/base/BaseVncActivity.java | 23 +++++++++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/VncInfoHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/VncInfoHandler.java index ba9215068..30c366e28 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/VncInfoHandler.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/VncInfoHandler.java @@ -32,5 +32,17 @@ public void handle(@NonNull ClientRequest request) throws Exception { res.put("host", !host.isEmpty() ? host : "127.0.0.1"); res.put("port", inst.item.optLong("vnc_port", -1)); res.put("password", inst.item.optString("vnc_password", "")); + // When VNC binds to the IPv4 wildcard, resolve the phone's own LAN + // address here from the router watcher's filtered host-IP set, which + // already drops pbridge offload-proxy addresses parked on the uplink. + // The client cannot exclude those itself: the offload tag is a netlink + // route metric invisible to java.net.NetworkInterface, so its naive + // interface enumeration would pick a guest proxy IP instead. + if ("0.0.0.0".equals(host)) { + var hostIps = request.getContext().getRouterWatcher().getHostIpv4Addresses(); + var it = hostIps.iterator(); + if (it.hasNext()) + res.put("remote_host", it.next()); + } } } diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/base/BaseVncActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/base/BaseVncActivity.java index f319b4221..9fafe5eb7 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/base/BaseVncActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/base/BaseVncActivity.java @@ -72,6 +72,10 @@ public abstract class BaseVncActivity extends AppCompatActivity implements ImeIn protected String vmName = ""; protected String vmId = ""; protected String vncHost = "127.0.0.1"; + // Phone LAN address the daemon resolved for an IPv4-wildcard bind (offload + // proxy IPs already excluded); empty when not applicable. Preferred over + // local interface enumeration for the external/remote connect address. + protected String vncRemoteHost = ""; protected int vncPort = DEFAULT_PORT; protected String vncPassword = null; protected volatile boolean running = false; @@ -223,6 +227,7 @@ protected void fetchVncInfoAndConnect() { }; DaemonConnection.OnResponse res = resp -> { vncHost = resp.optString("host", "127.0.0.1"); + vncRemoteHost = resp.optString("remote_host", ""); vncPort = resp.optInt("port", DEFAULT_PORT); vncPassword = resp.optString("password", ""); if (vncPassword.isEmpty()) vncPassword = null; @@ -594,10 +599,24 @@ protected void rotateScreen() { } } + /** + * Resolves the host for a VNC URI. For the external/remote address, prefers + * the daemon-resolved phone LAN address ({@link #vncRemoteHost}) when the VM + * binds the IPv4 wildcard, since it excludes pbridge offload-proxy IPs that + * local interface enumeration cannot tell apart. Falls back to + * {@link cn.classfun.droidvm.lib.utils.NetUtils#resolveAddress} otherwise. + */ + @NonNull + private String resolveVncHost(boolean local) { + if (!local && "0.0.0.0".equals(vncHost) && !vncRemoteHost.isEmpty()) + return vncRemoteHost; + return resolveAddress(vncHost, local); + } + @NonNull protected String generateVncUri(boolean local) { var sb = new StringBuilder(); - var host = resolveAddress(vncHost, local); + var host = resolveVncHost(local); var port = vncPort; sb.append(fmt("vnc://%s:%d/", host, port)); var ps = new StringBuilder(); @@ -610,7 +629,7 @@ protected String generateVncUri(boolean local) { protected void openWithExternalApp() { var url = generateVncUri(false); - var host = resolveAddress(vncHost, false); + var host = resolveVncHost(false); var target = fmt("%s:%d", host, vncPort); boolean hasPassword = vncPassword != null && !vncPassword.isEmpty(); var view = getLayoutInflater().inflate(R.layout.dialog_vnc_external, null); From de10129e3f7dc917a331392fea1d58a516566206 Mon Sep 17 00:00:00 2001 From: HuJK Date: Tue, 30 Jun 2026 09:50:34 +0800 Subject: [PATCH 069/182] vm: record created_at timestamp on VM creation The VM info screen already shows a "created" row; populate it by stamping created_at when a new VMConfig is constructed. Co-Authored-By: Claude Opus 4.8 --- app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMConfig.java | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMConfig.java b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMConfig.java index 7fae1bef5..4ef282125 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMConfig.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMConfig.java @@ -17,6 +17,7 @@ public class VMConfig extends DataConfig { public VMConfig() { setId(UUID.randomUUID()); + item.set("created_at", System.currentTimeMillis()); } public VMConfig(@NonNull JSONObject obj) throws JSONException { From 495b9ede3400bab9f6734731a170fd4e14856de2 Mon Sep 17 00:00:00 2001 From: HuJK Date: Tue, 30 Jun 2026 09:50:45 +0800 Subject: [PATCH 070/182] (feat) adjust hugepage pool size at runtime, see vm memory allocated Add a hugepage reserve-pool manager: live pool resize (shrink frees now, grow + acquire migrates pages in), per-VM attribution via the kernel module (KO) with a THP-scan fallback, kill/stack-trace actions, and a usage screen. The Disable button shrinks the pool to one page (freeing memory) without unloading, so per-VM tracking is never lost. Co-Authored-By: Claude Opus 4.8 --- app/src/main/AndroidManifest.xml | 7 + .../droidvm/ui/hugepage/HugePageActivity.java | 399 ++++++++-- .../droidvm/ui/hugepage/HugePageColor.java | 39 + .../droidvm/ui/hugepage/HugePageProcess.java | 66 ++ .../ui/hugepage/HugePageProcessActivity.java | 747 ++++++++++++++++++ .../ui/hugepage/HugePageProcessAdapter.java | 198 +++++ .../droidvm/ui/hugepage/SegmentedBar.java | 77 ++ .../main/res/drawable/ic_circle_filled.xml | 9 + .../main/res/drawable/ic_circle_outline.xml | 11 + app/src/main/res/layout/activity_hugepage.xml | 129 ++- .../res/layout/activity_hugepage_process.xml | 112 +++ .../main/res/layout/item_hugepage_process.xml | 112 +++ .../main/res/menu/menu_hugepage_process.xml | 20 + app/src/main/res/values-night/colors.xml | 2 + app/src/main/res/values-zh-rCN/strings.xml | 57 ++ app/src/main/res/values-zh-rTW/strings.xml | 63 +- app/src/main/res/values/colors.xml | 3 + app/src/main/res/values/strings.xml | 63 +- 18 files changed, 2015 insertions(+), 99 deletions(-) create mode 100644 app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageColor.java create mode 100644 app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcess.java create mode 100644 app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcessActivity.java create mode 100644 app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcessAdapter.java create mode 100644 app/src/main/java/cn/classfun/droidvm/ui/hugepage/SegmentedBar.java create mode 100644 app/src/main/res/drawable/ic_circle_filled.xml create mode 100644 app/src/main/res/drawable/ic_circle_outline.xml create mode 100644 app/src/main/res/layout/activity_hugepage_process.xml create mode 100644 app/src/main/res/layout/item_hugepage_process.xml create mode 100644 app/src/main/res/menu/menu_hugepage_process.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a384f6bba..65cd1970c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -184,6 +184,13 @@ android:theme="@style/Theme.DroidVM.NoActionBar" android:windowSoftInputMode="adjustResize" /> + + openModulePage()) - .setOnDismissListener(d -> finish()) - .show(); - return; - } toolbar.setTitle(R.string.hugepage_title); toolbar.setNavigationOnClickListener(v -> finish()); - btnManualRefill.setOnClickListener(v -> doManualRefill()); + btnSavePoolSize.setOnClickListener(v -> { + if (moduleAcquiring) interruptAcquire(); + else savePoolSize(); + }); rowModuleEnable.setOnCheckedChangeListener(this::doToggleModule); - btnStop.setOnClickListener(v -> confirmUnload()); - cardCrashWarning.setOnClickListener(v -> doDismissCrash()); - inputPoolSize.setOnFocusLostListener(this::savePoolSize); - inputPoolSize.setOnEditorActionListener((v, actionId, event) -> { - if (actionId == EditorInfo.IME_ACTION_DONE) { - savePoolSize(); - return true; - } - return false; + // One button: + // not installed -> Install (open releases page) + // installed, unloaded -> Enable (insmod) + // loaded, soft-disabled-> Enable (restore pool_want + acquire) + // loaded (v7) -> Disable (shrink pool to 1 page, stay loaded + // so per-VM tracking is never lost; no save) + // loaded (v6, no knob) -> Disable (rmmod) + btnModuleToggle.setOnClickListener(v -> { + if (!moduleInstalled) openModulePage(); + else if (!moduleLoaded) doLoad(); + else if (moduleSoftDisabled) doSoftEnable(); + else if (moduleHasPoolWant) doSoftDisable(); + else confirmUnload(); }); + btnViewProcesses.setOnClickListener(v -> startActivity( + new Intent(this, HugePageProcessActivity.class))); + cardCrashWarning.setOnClickListener(v -> doDismissCrash()); loadPoolSize(); } private void openModulePage() { - var url = "https://github.com/Droid-VM/gh-hugepage-reserve"; + var url = "https://github.com/Droid-VM/gh-hugepage-reserve/releases"; var intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); startActivity(intent); } @@ -141,7 +164,6 @@ protected void onPause() { super.onPause(); resumed = false; handler.removeCallbacks(refreshRunnable); - savePoolSize(); } private void scheduleRefresh() { @@ -150,22 +172,51 @@ private void scheduleRefresh() { private void refreshStatus() { runOnPool(() -> { + var moduleInst = shellCheckExists(MODULE_PROP); var moduleLoaded = shellCheckExists(SYSFS_BASE); var moduleDisabled = shellCheckExists(DISABLE_FILE); var crashStamp = shellCheckExists(CRASH_FILE); var refillStat = ""; + var servedSummary = ""; if (moduleLoaded) try { refillStat = shellReadFile(pathJoin(SYSFS_PARAMS, "refill_stat")); + // Reconcile + read per-VM served pages so this screen's bar shows + // the exact same per-VM breakdown as the usage screen's bar. + if (shellCheckExists(pathJoin(SYSFS_PARAMS, "served_summary"))) { + run("echo 1 > %s/reconcile", SYSFS_PARAMS); + servedSummary = shellReadFile(pathJoin(SYSFS_PARAMS, "served_summary")); + } } catch (Exception e) { Log.w(TAG, "Failed to read parameters", e); } var stats = parseProp(refillStat); + var owners = parseServedOwners(servedSummary); runOnUiThread(() -> updateUI( - moduleLoaded, moduleDisabled, crashStamp, stats + moduleInst, moduleLoaded, moduleDisabled, crashStamp, stats, owners )); }); } + /** Parse served_summary "owner ... pid=N pages=M" lines into {pid, pages}. */ + @NonNull + private List parseServedOwners(@NonNull String raw) { + var owners = new ArrayList(); + for (var line : raw.split("\n")) { + line = line.trim(); + if (!line.startsWith("owner ")) continue; + long pid = -1, pages = 0; + for (var tok : line.split("\\s+")) { + try { + if (tok.startsWith("pid=")) pid = Long.parseLong(tok.substring(4)); + else if (tok.startsWith("pages=")) pages = Long.parseLong(tok.substring(6)); + } catch (NumberFormatException ignored) { + } + } + if (pid > 0 && pages > 0) owners.add(new long[]{pid, pages}); + } + return owners; + } + @NonNull private Map parseProp(@NonNull String raw) { var map = new LinkedHashMap(); @@ -188,9 +239,10 @@ private long getPages(@NonNull Map stats, @NonNull String key) { } private void updateUI( - boolean loaded, + boolean installed, boolean loaded, boolean disabled, boolean crashed, - @NonNull Map stats + @NonNull Map stats, + @NonNull List owners ) { if (isFinishing()) return; cardCrashWarning.setVisibility(crashed ? VISIBLE : GONE); @@ -203,51 +255,200 @@ private void updateUI( try { var poolAvail = getPages(stats, "pool_avail"); var poolTotal = getPages(stats, "pool_total"); + // "total" shows the desired target (pool_want); grow raises it + // while capacity (pool_total) only rises via acquire. Old + // modules without pool_want fall back to capacity. + var poolWant = getPages(stats, "pool_want"); + if (poolWant <= 0) poolWant = poolTotal; + // Used = pages currently served to VMs (traced). Prefer the live + // served counter; pool_total - avail under-counts after a shrink + // that left served pages out. Old modules: fall back to that. + var used = stats.containsKey("served") + ? getPages(stats, "served") + : Math.clamp(poolTotal - poolAvail, 0, poolTotal); + // 2x2 caption (identical to the usage screen): used / available + // on top, total / pool-size below. Total = real held reserve + // (used + avail = owned + traced), shown raw - no clamp to the + // pool size, so a kernel that fails to release on shrink shows up + // as total > the size you set. + var held = used + poolAvail; + setPagesString(tvPoolUsed, R.string.hugepage_stat_pool_used, used); setPagesString(tvPoolAvail, R.string.hugepage_stat_pool_available, poolAvail); - setPagesString(tvPoolTotal, R.string.hugepage_stat_pool_total, poolTotal); - if (poolTotal > 0) { - var used = Math.clamp(poolTotal - poolAvail, 0, poolTotal); - var progress = (int) (used * 1000L / poolTotal); - progressPoolUsage.setProgress(progress); - } else { - progressPoolUsage.setProgress(0); + setPagesString(tvPoolTotal, R.string.hugepage_stat_pool_total, held); + setPagesString(tvPoolSize, R.string.hugepage_stat_pool_size, poolWant); + // Segmented bar: one colored segment per VM (same per-VM colours + // and deficit block as the usage screen's bar) + a track for the + // available portion; total = want. + boolean dark = HugePageColor.isDark(this); + int n = owners.size(); + int[] colors = new int[n + 1]; + float[] values = new float[n + 1]; + long seg = 0; + for (int i = 0; i < n; i++) { + int pid = (int) owners.get(i)[0]; + long ownerPages = owners.get(i)[1]; + colors[i] = HugePageColor.forPid(pid, dark); + values[i] = ownerPages; + seg += ownerPages; } + colors[n] = HugePageColor.pending(this); + values[n] = Math.max(0, poolWant - seg - poolAvail); + float total = Math.max(poolWant, seg + poolAvail); + segPoolBar.setData(colors, values, total); } catch (NumberFormatException e) { + tvPoolUsed.setText("-"); tvPoolAvail.setText("-"); tvPoolTotal.setText("-"); - progressPoolUsage.setProgress(0); + tvPoolSize.setText("-"); + segPoolBar.setData(new int[0], new float[0], 0f); } } else { rowStatState.setValue(getString(R.string.hugepage_stats_unavailable)); rowStatTotalServed.setValue(null); rowStatTotalRefilled.setValue(null); rowStatActiveVms.setValue(null); + tvPoolUsed.setText("-"); tvPoolAvail.setText("-"); tvPoolTotal.setText("-"); - progressPoolUsage.setProgress(0); + tvPoolSize.setText("-"); + segPoolBar.setData(new int[0], new float[0], 0f); } - btnManualRefill.setEnabled(loaded); - btnStop.setEnabled(loaded); + // While an acquire runs the Save button shows a spinner (label hidden, + // floppy icon kept) but stays pressable: tapping it then interrupts the + // acquire, keeping the current size (see the click handler). pool_want is + // settable any time now, so neither button needs to be disabled. + boolean acquiring = loaded && "1".equals(stats.get("acquire_active")); + moduleAcquiring = acquiring; + progressSavePoolSize.setVisibility(acquiring ? VISIBLE : GONE); + if (acquiring) btnSavePoolSize.setTextColor(Color.TRANSPARENT); + else btnSavePoolSize.setTextColor(saveTextColors); + btnSavePoolSize.setEnabled(installed); + // Install / Enable / Disable. "Disable" on v7 shrinks the pool to one + // page (frees memory) but keeps the module loaded so it never loses + // per-VM tracking; soft-disabled shows "Enable" again. v6 has no + // pool_want knob, so Disable falls back to rmmod. + moduleInstalled = installed; + moduleLoaded = loaded; + moduleHasPoolWant = stats.containsKey("pool_want"); + moduleSoftDisabled = loaded && moduleHasPoolWant + && getPages(stats, "pool_want") <= 1; + btnModuleToggle.setEnabled(true); + if (!installed) { + btnModuleToggle.setText(R.string.hugepage_btn_install); + btnModuleToggle.setIconResource(R.drawable.ic_download); + } else if (!loaded || moduleSoftDisabled) { + btnModuleToggle.setText(R.string.hugepage_btn_enable); + btnModuleToggle.setIconResource(R.drawable.ic_start); + } else { + btnModuleToggle.setText(R.string.hugepage_btn_disable); + btnModuleToggle.setIconResource(R.drawable.ic_stop); + } + rowModuleEnable.setEnabled(installed); rowModuleEnable.setChecked(!disabled); } - private void doManualRefill() { - btnManualRefill.setEnabled(false); + private void doLoad() { + btnModuleToggle.setEnabled(false); runOnPool(() -> { - var result = run( - "echo 1 > %s/manual_refill", - SYSFS_PARAMS - ).isSuccess(); + if (shellCheckExists(SYSFS_BASE)) { + runOnUiThread(() -> { + Toast.makeText(this, R.string.hugepage_already_loaded, + LENGTH_SHORT).show(); + refreshStatus(); + }); + return; + } + // Read the configured size from settings.prop in Java (rather than + // sourcing it in shell). v7 reads pool_want, older builds pool_target. + var size = "1024"; + if (shellCheckExists(SETTINGS_PROP)) { + var settings = parseProp(shellReadFile(SETTINGS_PROP)); + size = settings.getOrDefault("pool_want", + settings.getOrDefault("pool_target", "1024")); + } + // Insmod compatibly across versions: v7 takes pool_want, older builds + // take pool_target, anything else loads bare. First success wins. + var ko = pathJoin(MAGISK_BASE, "gh_hugepage_reserve.ko"); + var result = + run("insmod \"%s\" pool_want=\"%s\"", ko, size).isSuccess() + || run("insmod \"%s\" pool_target=\"%s\"", ko, size).isSuccess() + || run("insmod \"%s\"", ko).isSuccess(); runOnUiThread(() -> { - btnManualRefill.setEnabled(true); - var msg = result ? - R.string.hugepage_refill_triggered : - R.string.hugepage_refill_failed; - Toast.makeText(this, msg, LENGTH_SHORT).show(); + Toast.makeText(this, result + ? R.string.hugepage_loaded + : R.string.hugepage_load_failed, + LENGTH_SHORT).show(); + refreshStatus(); }); }); } + /** + * Soft-disable (v7): shrink the pool to a single page so the reserved memory + * is freed back to the system, but keep the module loaded so it never loses + * per-VM tracking (unlike rmmod). settings.prop is left untouched, so the + * next boot still allocates the configured capacity. + */ + private void doSoftDisable() { + btnModuleToggle.setEnabled(false); + runOnPool(() -> { + // The module refuses a resize mid-acquire, so stop the worker first. + stopAcquireAndWait(); + run("echo 1 > %s/pool_want", SYSFS_PARAMS); + runOnUiThread(this::refreshStatus); + }); + } + + /** + * Interrupt a running acquire: write 0 to the acquire knob. pool_want is left + * intact, so the worker stops at the size reached so far and the remaining + * deficit keeps showing as "waiting for acquire". + */ + private void interruptAcquire() { + runOnPool(() -> { + run("echo 0 > %s/acquire", SYSFS_PARAMS); + runOnUiThread(this::refreshStatus); + }); + } + + /** + * Interrupt any running acquire and block (on the pool thread) until the + * worker is quiescent. pool_want can't be set while an acquire runs, so call + * this before writing pool_want. The worker can be mid-migration, so this may + * take a moment; bounded so a wedged worker can't hang us forever. + */ + private void stopAcquireAndWait() { + if (!shellCheckExists(pathJoin(SYSFS_PARAMS, "acquire"))) return; + run("echo 0 > %s/acquire", SYSFS_PARAMS); + for (int i = 0; i < 60; i++) { + var st = parseProp(shellReadFile(pathJoin(SYSFS_PARAMS, "refill_stat"))); + if (!"1".equals(st.get("acquire_active"))) return; + try { + Thread.sleep(500); + } catch (InterruptedException ignored) { + return; + } + } + } + + /** Re-enable a soft-disabled pool: restore the configured size and refill. */ + private void doSoftEnable() { + btnModuleToggle.setEnabled(false); + runOnPool(() -> { + var size = "1024"; + if (shellCheckExists(SETTINGS_PROP)) { + var s = parseProp(shellReadFile(SETTINGS_PROP)); + size = s.getOrDefault("pool_want", + s.getOrDefault("pool_target", "1024")); + } + run("echo %s > %s/pool_want", size, SYSFS_PARAMS); + if (shellCheckExists(pathJoin(SYSFS_PARAMS, "acquire"))) { + run("echo 1 > %s/acquire", SYSFS_PARAMS); + } + runOnUiThread(this::refreshStatus); + }); + } + private void loadPoolSize() { runOnPool(() -> { Map settings; @@ -258,32 +459,102 @@ private void loadPoolSize() { Log.w(TAG, "Failed to read settings.prop", e); return; } - var cur = settings.getOrDefault("pool_target", "2048"); - if (cur == null || cur.isEmpty()) cur = "2048"; + // Prefer pool_want; fall back to legacy pool_target. + var cur = settings.getOrDefault("pool_want", + settings.getOrDefault("pool_target", "1024")); + if (cur == null || cur.isEmpty()) cur = "1024"; try { var pages = Long.parseLong(cur); var bytes = BigInteger.valueOf(pages * PAGE_SIZE); runOnUiThread(() -> inputPoolSize.setBigValue(bytes)); } catch (NumberFormatException e) { - Log.w(TAG, "Failed to parse pool_target", e); + Log.w(TAG, "Failed to parse pool_want", e); } }); } + /** + * Sum of configured memory (MiB) over every VM that currently has a live + * process (state RUNNING/STARTING/SUSPENDED). Blocks briefly on the daemon; + * call from a background thread. 0 if none / daemon unreachable. + */ + private long runningVmMemMib() { + var total = new long[]{0}; + var latch = new CountDownLatch(1); + DaemonConnection.getInstance().buildRequest("vm_list") + .onResponse(resp -> { + try { + var arr = resp.optJSONArray("data"); + if (arr != null) { + for (int i = 0; i < arr.length(); i++) { + var vm = arr.optJSONObject(i); + if (vm == null) continue; + var state = vm.optString("state", "STOPPED"); + if ("RUNNING".equals(state) || "STARTING".equals(state) + || "SUSPENDED".equals(state)) { + total[0] += vm.optLong("memory_mb", 0); + } + } + } + } finally { + latch.countDown(); + } + }) + .onError(e -> latch.countDown()) + .invoke(); + try { + //noinspection ResultOfMethodCallIgnored + latch.await(2, TimeUnit.SECONDS); + } catch (InterruptedException ignored) { + } + return total[0]; + } + private void savePoolSize() { if (!inputPoolSize.isInputValid()) return; var bytes = inputPoolSize.getBigValue(); var pages = bytes.divide(BigInteger.valueOf(PAGE_SIZE)); runOnPool(() -> { - var result = run( - "echo 'pool_target=%s' > %s", - pages.toString(), SETTINGS_PROP - ).isSuccess(); + // The pool must be able to back every running VM's RAM, so it can't + // be set below the sum of running VMs' configured memory. + long needMib = runningVmMemMib(); + long wantMib = pages.longValue() * (PAGE_SIZE / (1024 * 1024)); + if (needMib > 0 && wantMib < needMib) { + runOnUiThread(() -> Toast.makeText(this, + getString(R.string.hugepage_pool_below_vms, + SizeUtils.formatSize(needMib * 1024 * 1024)), + LENGTH_SHORT).show()); + return; + } + // Persist for the next load. Write BOTH keys so the size survives + // whichever module/loader is installed at boot: v7's loader reads + // pool_want, an older (v6) package's loader reads pool_target. + var wroteWant = run("echo 'pool_want=%s' > %s", + pages.toString(), SETTINGS_PROP).isSuccess(); + var wroteTarget = run("echo 'pool_target=%s' >> %s", + pages.toString(), SETTINGS_PROP).isSuccess(); + var result = wroteWant && wroteTarget; + // Apply at runtime via the single pool_want knob: shrink frees excess + // now, grow only raises the target (Acquire fills it). Old modules + // without pool_want just keep the saved value (effective next reboot). + var applied = false; + if (shellCheckExists(pathJoin(SYSFS_PARAMS, "pool_want"))) { + applied = run("echo %s > %s/pool_want", + pages.toString(), SYSFS_PARAMS).isSuccess(); + // Saving also kicks off one acquire so a grown target starts + // filling immediately (a shrink just makes the worker exit at + // once). The 1 s status poll then animates the bar + Save spinner. + if (applied && shellCheckExists(pathJoin(SYSFS_PARAMS, "acquire"))) { + run("echo 1 > %s/acquire", SYSFS_PARAMS); + } + } + var finalApplied = applied; runOnUiThread(() -> { - var msg = result ? - R.string.hugepage_pool_size_saved : - R.string.hugepage_pool_size_failed; + int msg = !result ? R.string.hugepage_pool_size_failed + : finalApplied ? R.string.hugepage_pool_size_applied + : R.string.hugepage_pool_size_saved; Toast.makeText(this, msg, LENGTH_SHORT).show(); + refreshStatus(); }); }); } @@ -317,7 +588,7 @@ private void confirmUnload() { } private void doUnload() { - btnStop.setEnabled(false); + btnModuleToggle.setEnabled(false); runOnPool(() -> { var result = runList("rmmod", "gh_hugepage_reserve").isSuccess(); runOnUiThread(() -> { diff --git a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageColor.java b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageColor.java new file mode 100644 index 000000000..19f71c334 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageColor.java @@ -0,0 +1,39 @@ +package cn.classfun.droidvm.ui.hugepage; + +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Color; + +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; + +import cn.classfun.droidvm.R; + +/** + * Deterministic per-process colors for the hugepage usage bar and list icons. + * The same pid always maps to the same hue (stable across refreshes); in dark + * mode colors are light, in light mode they are dark, so they read against the + * surface either way. + */ +final class HugePageColor { + private HugePageColor() { + } + + static boolean isDark(Context ctx) { + int mode = ctx.getResources().getConfiguration().uiMode + & Configuration.UI_MODE_NIGHT_MASK; + return mode == Configuration.UI_MODE_NIGHT_YES; + } + + /** Golden-angle hue spread keyed by pid, theme-aware saturation/value. */ + static int forPid(int pid, boolean dark) { + float hue = ((pid * 137.508f) % 360f + 360f) % 360f; + float[] hsv = {hue, dark ? 0.50f : 0.72f, dark ? 0.90f : 0.55f}; + return Color.HSVToColor(hsv); + } + + /** Color for the "waiting for acquire" capacity-deficit portion. */ + static int pending(@NonNull Context ctx) { + return ContextCompat.getColor(ctx, R.color.hugepage_pending); + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcess.java b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcess.java new file mode 100644 index 000000000..c8ebec89f --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcess.java @@ -0,0 +1,66 @@ +package cn.classfun.droidvm.ui.hugepage; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * A single process that the gh_hugepage_reserve module has served pool pages to. + * Sourced from the module's {@code vm_owners} sysfs file and enriched with live + * /proc state. {@link #vmName} is a best-effort match against running VMs and may + * be null for non-DroidVM processes that still consume the reserved pool. + */ +public final class HugePageProcess { + public final int pid; + @NonNull + public final String comm; + /** + * Pool pages served to this owner (cumulative, 2MiB each), or -1 when this + * row came from a system-wide /proc scan rather than module attribution. + */ + public final long servedPages; + /** + * Current THP-backed memory mapped by the process, in kB (-1 if unknown). + * Sum of AnonHugePages + ShmemPmdMapped: crosvm guest RAM is shmem-backed + * THP (ShmemPmdMapped), so AnonHugePages alone is 0 for VMs. + */ + public final long thpKb; + /** Process state char from /proc//status, e.g. 'R','S','D' ('?' unknown). */ + public final char state; + /** Matched VM name, or null when no running VM owns this pid. */ + @Nullable + public final String vmName; + /** Whether /proc/ still exists (process alive). */ + public final boolean alive; + /** Assigned display color (icon tint + bar segment). */ + public final int color; + /** True for the synthetic "unattributed" pool usage row (no actions). */ + public final boolean unknown; + /** True for the synthetic "waiting for acquire" deficit row (acquire btn). */ + public final boolean acquire; + + public HugePageProcess( + int pid, @NonNull String comm, long servedPages, + long thpKb, char state, @Nullable String vmName, boolean alive, + int color, boolean unknown, boolean acquire + ) { + this.pid = pid; + this.comm = comm; + this.servedPages = servedPages; + this.thpKb = thpKb; + this.state = state; + this.vmName = vmName; + this.alive = alive; + this.color = color; + this.unknown = unknown; + this.acquire = acquire; + } + + /** THP occupancy expressed in 2MiB pages (-1 if unknown). */ + public long thpPages() { + return thpKb < 0 ? -1 : thpKb / 2048; + } + + public boolean isUninterruptibleSleep() { + return state == 'D'; + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcessActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcessActivity.java new file mode 100644 index 000000000..b23b37752 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcessActivity.java @@ -0,0 +1,747 @@ +package cn.classfun.droidvm.ui.hugepage; + +import static android.view.View.GONE; +import static android.view.View.VISIBLE; +import static android.widget.Toast.LENGTH_LONG; +import static android.widget.Toast.LENGTH_SHORT; +import static cn.classfun.droidvm.lib.utils.FileUtils.shellCheckExists; +import static cn.classfun.droidvm.lib.utils.FileUtils.shellReadFile; +import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; +import static cn.classfun.droidvm.lib.utils.ProcessUtils.SIGKILL; +import static cn.classfun.droidvm.lib.utils.ProcessUtils.SIGTERM; +import static cn.classfun.droidvm.lib.utils.ProcessUtils.shellKillProcess; +import static cn.classfun.droidvm.lib.utils.RunUtils.run; +import static cn.classfun.droidvm.lib.utils.StringUtils.pathJoin; +import static cn.classfun.droidvm.lib.utils.ThreadUtils.runOnPool; + +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import android.view.MenuItem; +import android.widget.TextView; +import android.widget.Toast; + +import android.annotation.SuppressLint; + +import androidx.annotation.NonNull; +import androidx.appcompat.view.menu.MenuBuilder; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.appbar.MaterialToolbar; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import cn.classfun.droidvm.R; +import cn.classfun.droidvm.lib.daemon.DaemonConnection; +import cn.classfun.droidvm.lib.size.SizeUtils; + +/** + * Lists the processes the gh_hugepage_reserve module has served pool pages to, + * sourced from the module's {@code vm_owners} sysfs file and enriched with live + * /proc state. Each row offers a kill action (SIGTERM/SIGKILL); processes stuck + * in uninterruptible sleep (D state) expose their kernel stack. + */ +public final class HugePageProcessActivity extends AppCompatActivity + implements HugePageProcessAdapter.Listener { + private static final String TAG = "HugePageProcessActivity"; + private static final String SYSFS_BASE = "/sys/module/gh_hugepage_reserve"; + private static final String SYSFS_PARAMS = pathJoin(SYSFS_BASE, "parameters"); + private static final String SYSFS_OWNERS = pathJoin(SYSFS_PARAMS, "vm_owners"); + private static final long REFRESH_INTERVAL_MS = 2000; + /** Acquire progress poll: ~500 ms ticks, capped so a wedged run can't poll forever. */ + private static final long ACQUIRE_POLL_MS = 500; + private static final int ACQUIRE_POLL_MAX = 1200; // ~10 min ceiling + private static final long HUGE_PAGE_BYTES = 2L * 1024 * 1024; + + private final Handler handler = new Handler(Looper.getMainLooper()); + private boolean resumed = false; + private boolean scanMode = false; + private boolean firstRefresh = true; + private volatile boolean acquireWatching = false; + // The KO-vs-THP fallback is resolved at most once (on entry). After that, + // and after any manual menu choice, we never auto-switch again - otherwise a + // single flaky root `test -e` under load would permanently bounce KO->THP. + private volatile boolean modeResolved = false; + private MaterialToolbar toolbar; + private RecyclerView recyclerView; + private TextView tvEmpty; + private TextView tvUsed; + private TextView tvAvail; + private TextView tvTotal; + private TextView tvPoolSize; + private SegmentedBar segBar; + private HugePageProcessAdapter adapter; + + private final Runnable refreshRunnable = () -> { + refresh(); + scheduleRefresh(); + }; + + @Override + @SuppressLint("RestrictedApi") // MenuBuilder.setOptionalIconsVisible + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_hugepage_process); + toolbar = findViewById(R.id.toolbar); + recyclerView = findViewById(R.id.recycler); + tvEmpty = findViewById(R.id.tv_empty); + tvUsed = findViewById(R.id.tv_used); + tvAvail = findViewById(R.id.tv_avail); + tvTotal = findViewById(R.id.tv_total); + tvPoolSize = findViewById(R.id.tv_pool_size); + segBar = findViewById(R.id.seg_bar); + toolbar.setTitle(R.string.hugepage_proc_title_screen); + toolbar.setNavigationOnClickListener(v -> finish()); + adapter = new HugePageProcessAdapter(this); + recyclerView.setLayoutManager(new LinearLayoutManager(this)); + recyclerView.setAdapter(adapter); + + // Always default to KO (module attribution) on entry; refresh() auto + // switches to THP scan only when the module is unusable. + scanMode = false; + toolbar.inflateMenu(R.menu.menu_hugepage_process); + // Material hides menu-item icons in the overflow popup by default; force + // them on so the active-mode check icon is visible. + var menu = toolbar.getMenu(); + if (menu instanceof MenuBuilder) { + ((MenuBuilder) menu).setOptionalIconsVisible(true); + } + updateModeChecked(); + toolbar.setOnMenuItemClickListener(item -> { + int id = item.getItemId(); + if (id == R.id.menu_mode_ko) { + setMode(false); + return true; + } + if (id == R.id.menu_mode_scan) { + setMode(true); + return true; + } + return false; + }); + } + + private void updateModeChecked() { + // Radio-style: filled circle = active mode, hollow circle = inactive. + // (overflow icons are forced visible in onCreate; a checkableBehavior + // radio didn't redraw here, and glyphs would be non-ASCII in code.) + var menu = toolbar.getMenu(); + var ko = menu.findItem(R.id.menu_mode_ko); + var scan = menu.findItem(R.id.menu_mode_scan); + if (ko != null) ko.setIcon(modeIcon(!scanMode)); + if (scan != null) scan.setIcon(modeIcon(scanMode)); + } + + private static int modeIcon(boolean active) { + return active ? R.drawable.ic_circle_filled : R.drawable.ic_circle_outline; + } + + /** User picked a mode from the menu (session-only; entry always defaults KO). */ + private void setMode(boolean scan) { + scanMode = scan; + modeResolved = true; // explicit choice: never auto-switch again + updateModeChecked(); + refresh(); + } + + /** + * KO was selected but its attribution API is absent (module not loaded, or + * an old .ko). Switch the effective mode to scan for this session without + * overwriting any saved preference, and tell the user why. A present-but- + * empty pool (no VM) never reaches here - that stays in KO. + */ + private void fallbackToScan() { + if (!scanMode) { + scanMode = true; + updateModeChecked(); + Toast.makeText(this, R.string.hugepage_proc_ko_unavailable, + LENGTH_SHORT).show(); + } + } + + + @Override + protected void onResume() { + super.onResume(); + resumed = true; + refresh(); + scheduleRefresh(); + } + + @Override + protected void onPause() { + super.onPause(); + resumed = false; + handler.removeCallbacks(refreshRunnable); + } + + private void scheduleRefresh() { + if (resumed) handler.postDelayed(refreshRunnable, REFRESH_INTERVAL_MS); + } + + /* ================================================================== */ + /* Data collection */ + /* ================================================================== */ + + private void refresh() { + // Only the very first load shows a "scanning" placeholder. Every later + // refresh runs entirely in the background and atomically swaps the + // results in via showResult(), so the list never blanks mid-scan. + if (firstRefresh) { + recyclerView.setVisibility(GONE); + tvEmpty.setText(R.string.hugepage_proc_loading); + tvEmpty.setVisibility(VISIBLE); + } + runOnPool(() -> { + boolean dark = HugePageColor.isDark(this); + boolean useScan = scanMode; + boolean autoSwitched = false; + // ONE-SHOT: only on the first resolve do we consider falling back to + // scan when the KO attribution API is absent (module not loaded, or + // an old .ko without served_summary). After that we never re-probe, + // so a transient root `test -e` failure can't flip a working KO view + // to THP. A present-but-empty pool (no VM) stays in KO ("no VM"). + if (!modeResolved) { + if (!useScan && !koApiPresent() && !koApiPresent()) { + // Probed twice (a single root `test -e` can flake under load); + // only fall back to scan if the KO API is really absent. + useScan = true; + autoSwitched = true; + } + modeResolved = true; + } + + List list = new ArrayList<>(); + String emptyText; + if (useScan) { + // Scan mode: system-wide THP (Anon + Shmem), no module needed. + try { + list = scanProc(dark); + } catch (Exception e) { + Log.w(TAG, "Scan failed", e); + } + emptyText = getString(R.string.hugepage_proc_scan_empty); + } else { + // Reconcile the served-page table on every query, then read the + // reconciled per-owner live page counts. + var livePages = new HashMap(); + try { + run("echo 1 > %s/reconcile", SYSFS_PARAMS); + var sum = shellReadFile(pathJoin(SYSFS_PARAMS, "served_summary")); + for (var ln : sum.split("\n")) { + ln = ln.trim(); + if (!ln.startsWith("owner ")) continue; + int pid = -1; + long pages = 0; + for (var tok : ln.split("\\s+")) { + if (tok.startsWith("pid=")) pid = Integer.parseInt(tok.substring(4)); + else if (tok.startsWith("pages=")) pages = Long.parseLong(tok.substring(6)); + } + if (pid > 0) livePages.put(pid, pages); + } + } catch (Exception e) { + Log.w(TAG, "reconcile/served_summary unavailable (old .ko?)", e); + } + try { + var owners = parseOwners(shellReadFile(SYSFS_OWNERS)); + if (!owners.isEmpty()) list = buildFromOwners(owners, dark, livePages); + } catch (Exception e) { + Log.w(TAG, "Failed to collect hugepage owners", e); + } + // Reached only when the KO API is present (else we auto-switched + // to scan above), so an empty list genuinely means "no VM". + emptyText = getString(R.string.hugepage_proc_owner_empty); + } + + // Three-value model: hold (pages in pool) + traced (sum of all + // applied/served processes) + waiting-to-acquire = want. + // traced = sum of listed rows' occupancy + // hold = pool_avail + // want = pool_want (grow target) + // waiting = max(0, want - hold - traced) <- one segment, no + // separate "unattributed"; it absorbs grow-deficit and + // orphaned (served-but-owner-gone) pages alike. + long tracedKb = 0; + for (var r : list) if (r.thpKb > 0) tracedKb += r.thpKb; + long holdKb, wantKb; + var pool = readPoolPages(); + boolean kernelAcquiring = pool != null && pool.length >= 4 && pool[3] != 0; + if (pool != null) { + holdKb = pool[1] * 2048; // avail + wantKb = pool[2] * 2048; // want + } else { + holdKb = 0; + wantKb = tracedKb; + } + long deficitKb = wantKb - holdKb - tracedKb; + if (deficitKb > 0) { + list.add(new HugePageProcess( + 0, getString(R.string.hugepage_proc_deficit), -1, deficitKb, + '?', null, true, HugePageColor.pending(this), false, true)); + } + + var finalList = list; + var finalEmpty = emptyText; + var finalAuto = autoSwitched; + var fTraced = tracedKb; + var fHold = holdKb; + var fWant = wantKb; + var fAcquiring = kernelAcquiring; + runOnUiThread(() -> { + if (finalAuto) fallbackToScan(); + // Re-sync the menu radio every refresh. Doing it once in onCreate + // (right after inflateMenu, before the menu is laid out) doesn't + // stick, leaving the radio showing the wrong mode. + updateModeChecked(); + adapter.setAcquiring(fAcquiring || acquireWatching); + showResult(finalList, fTraced, fHold, fWant, finalEmpty); + }); + }); + } + + /** True if the loaded module exposes the KO attribution API (v7+). */ + private boolean koApiPresent() { + return shellCheckExists(SYSFS_BASE) + && shellCheckExists(pathJoin(SYSFS_PARAMS, "served_summary")); + } + + @Nullable + private long[] readPoolPages() { + try { + if (!shellCheckExists(SYSFS_BASE)) return null; + var raw = shellReadFile(pathJoin(SYSFS_PARAMS, "refill_stat")); + long total = -1, avail = 0, want = -1, acquiring = 0, served = 0; + for (var line : raw.split("\n")) { + line = line.trim(); + if (line.startsWith("pool_total=")) { + total = Long.parseLong(line.substring(11).trim()); + } else if (line.startsWith("pool_avail=")) { + avail = Long.parseLong(line.substring(11).trim()); + } else if (line.startsWith("pool_want=")) { + want = Long.parseLong(line.substring(10).trim()); + } else if (line.startsWith("acquire_active=")) { + acquiring = Long.parseLong(line.substring(15).trim()); + } else if (line.startsWith("served=")) { + served = Long.parseLong(line.substring(7).trim()); + } + } + if (total < 0) return null; + // Old modules have no pool_want -> treat want == capacity. + if (want < 0) want = total; + return new long[]{total, avail, want, acquiring, served}; + } catch (Exception e) { + return null; + } + } + + /** + * Batch-read live /proc state (R/S/D + liveness) and THP occupancy + * (AnonHugePages + ShmemPmdMapped) for the given pids in a single root call. + * Shared by KO attribution (buildFromOwners) and THP scan (scanProc). + */ + @NonNull + private Map readProcInfo(@NonNull Collection pids) { + if (pids.isEmpty()) return new HashMap<>(); + var pidList = new StringBuilder(); + for (var pid : pids) { + if (pidList.length() > 0) pidList.append(' '); + pidList.append(pid); + } + var procRaw = run( + "for p in %s; do echo \"@@@ $p\"; " + + "grep -E '^State:' /proc/$p/status 2>/dev/null; " + + "grep -E '^AnonHugePages:|^ShmemPmdMapped:' " + + "/proc/$p/smaps_rollup 2>/dev/null; " + + "done", + pidList.toString() + ).getOutString(); + return parseProcInfo(procRaw); + } + + @NonNull + private List buildFromOwners( + @NonNull List owners, boolean dark, + @NonNull Map livePages + ) { + var pids = new ArrayList(); + for (var o : owners) pids.add(o.pid); + var procInfo = readProcInfo(pids); + + // Best-effort map pid -> VM name (only running VMs have a live pid). + var vmMap = fetchVmMap(); + + var result = new ArrayList(); + for (var o : owners) { + var info = procInfo.get(o.pid); + char state = info != null ? info.state : '?'; + boolean alive = info != null && info.alive; + // Occupancy = traced (reconciled served-page count for this owner), + // not the /proc THP scan. /proc is only used for state + liveness. + Long served = livePages.get(o.pid); + long tracedKb = (served != null ? served : 0L) * 2048; + result.add(new HugePageProcess( + o.pid, o.comm, -1, tracedKb, state, + vmMap.get(o.pid), alive, HugePageColor.forPid(o.pid, dark), + false, false)); + } + result.sort((a, b) -> Long.compare(b.thpKb, a.thpKb)); + return result; + } + + /** + * VM-only "scan": list the daemon's running VM processes and their actual + * THP occupancy (AnonHugePages + ShmemPmdMapped) from smaps. We deliberately + * do NOT scan all of /proc - system-wide THP (launchers, etc.) is noise that + * has nothing to do with the reserve pool. Works without the kernel module + * (ground-truth occupancy straight from each VM's smaps). + */ + @NonNull + private List scanProc(boolean dark) { + // Running VMs (pid -> name) come from the daemon. + var vmMap = fetchVmMap(); + if (vmMap.isEmpty()) return new ArrayList<>(); + + var procInfo = readProcInfo(vmMap.keySet()); + + var result = new ArrayList(); + for (var e : vmMap.entrySet()) { + int pid = e.getKey(); + var info = procInfo.get(pid); + char state = info != null ? info.state : '?'; + long thp = info != null ? info.thpKb : -1; + boolean alive = info != null && info.alive; + result.add(new HugePageProcess( + pid, e.getValue(), -1, thp, state, e.getValue(), alive, + HugePageColor.forPid(pid, dark), false, false)); + } + result.sort((a, b) -> Long.compare(b.thpKb, a.thpKb)); + return result; + } + + private static final class Owner { + final int pid; + final String comm; + final long served; + + Owner(int pid, String comm, long served) { + this.pid = pid; + this.comm = comm; + this.served = served; + } + } + + private static final class ProcInfo { + final char state; + final long thpKb; + final boolean alive; + + ProcInfo(char state, long thpKb, boolean alive) { + this.state = state; + this.thpKb = thpKb; + this.alive = alive; + } + } + + @NonNull + private List parseOwners(@NonNull String raw) { + var list = new ArrayList(); + for (var line : raw.split("\n")) { + line = line.trim(); + if (line.isEmpty()) continue; + // Format: pid= served= comm= + int pid = -1; + long served = 0; + String comm = "?"; + int commIdx = line.indexOf("comm="); + if (commIdx >= 0) comm = line.substring(commIdx + 5).trim(); + for (var tok : line.split("\\s+")) { + if (tok.startsWith("pid=")) { + try { + pid = Integer.parseInt(tok.substring(4)); + } catch (NumberFormatException ignored) { + } + } else if (tok.startsWith("served=")) { + try { + served = Long.parseLong(tok.substring(7)); + } catch (NumberFormatException ignored) { + } + } + } + if (pid > 0) list.add(new Owner(pid, comm, served)); + } + return list; + } + + @NonNull + private Map parseProcInfo(@NonNull String raw) { + var map = new HashMap(); + int curPid = -1; + char state = '?'; + long thp = -1; // -1 = unknown; becomes sum of Anon + Shmem THP + boolean alive = false; + for (var line : raw.split("\n")) { + if (line.startsWith("@@@ ")) { + if (curPid > 0) + map.put(curPid, new ProcInfo(state, thp, alive)); + try { + curPid = Integer.parseInt(line.substring(4).trim()); + } catch (NumberFormatException e) { + curPid = -1; + } + state = '?'; + thp = -1; + alive = false; + } else if (line.startsWith("State:")) { + var v = line.substring(6).trim(); + if (!v.isEmpty()) state = v.charAt(0); + alive = true; // status readable -> process exists + } else if (line.startsWith("AnonHugePages:") + || line.startsWith("ShmemPmdMapped:")) { + var digits = line.replaceAll("[^0-9]", ""); + if (!digits.isEmpty()) { + try { + thp = (thp < 0 ? 0 : thp) + Long.parseLong(digits); + } catch (NumberFormatException ignored) { + } + } + } + } + if (curPid > 0) map.put(curPid, new ProcInfo(state, thp, alive)); + return map; + } + + @NonNull + private Map fetchVmMap() { + var map = new HashMap(); + var latch = new CountDownLatch(1); + DaemonConnection.getInstance().buildRequest("vm_list") + .onResponse(resp -> { + try { + var arr = resp.optJSONArray("data"); + if (arr != null) { + for (int i = 0; i < arr.length(); i++) { + var obj = arr.optJSONObject(i); + if (obj == null) continue; + int pid = obj.optInt("pid", -1); + var name = obj.optString("name", null); + if (pid > 0 && name != null) map.put(pid, name); + } + } + } finally { + latch.countDown(); + } + }) + .onError(e -> latch.countDown()) + .invoke(); + try { + //noinspection ResultOfMethodCallIgnored + latch.await(2, TimeUnit.SECONDS); + } catch (InterruptedException ignored) { + } + return map; + } + + private void showResult( + @NonNull List list, long usedKb, long availKb, + long wantKb, @NonNull String emptyText + ) { + if (isFinishing()) return; + firstRefresh = false; + adapter.submit(list); + + // Segmented bar: one colored segment per row, including the + // "waiting for acquire" (deficit) row. Track = available; total = want. + int n = list.size(); + int[] colors = new int[n]; + float[] values = new float[n]; + long seg = 0; + for (int i = 0; i < n; i++) { + colors[i] = list.get(i).color; + values[i] = Math.max(0, list.get(i).thpKb); + seg += values[i]; + } + float total = Math.max(wantKb, seg + Math.max(0, availKb)); + segBar.setData(colors, values, total); + + // 2x2 caption: used / available on top, total / pool-size below. + // Total = the real held reserve (used + avail = owned + traced), shown + // raw with no clamp to pool size: if the kernel ever fails to release + // pages on a shrink, total exceeds the pool size you set, surfacing it + // instead of hiding it. + long heldKb = usedKb + availKb; + tvUsed.setText(getString(R.string.hugepage_stat_pool_used, + usedKb / 2048, SizeUtils.formatSize(usedKb * 1024))); + tvAvail.setText(getString(R.string.hugepage_stat_pool_available, + availKb / 2048, SizeUtils.formatSize(availKb * 1024))); + tvTotal.setText(getString(R.string.hugepage_stat_pool_total, + heldKb / 2048, SizeUtils.formatSize(heldKb * 1024))); + tvPoolSize.setText(getString(R.string.hugepage_stat_pool_size, + wantKb / 2048, SizeUtils.formatSize(wantKb * 1024))); + + if (list.isEmpty()) { + tvEmpty.setText(emptyText); + tvEmpty.setVisibility(VISIBLE); + recyclerView.setVisibility(GONE); + } else { + tvEmpty.setVisibility(GONE); + recyclerView.setVisibility(VISIBLE); + } + } + + /* ================================================================== */ + /* Actions */ + /* ================================================================== */ + + @Override + public void onKill(@NonNull HugePageProcess proc) { + var label = getString(R.string.hugepage_proc_title, proc.comm, proc.pid); + var signals = new String[]{ + getString(R.string.hugepage_proc_sigterm), + getString(R.string.hugepage_proc_sigkill), + }; + final int[] sel = {0}; // default SIGTERM + new MaterialAlertDialogBuilder(this) + .setTitle(getString(R.string.hugepage_proc_kill_title, label)) + .setSingleChoiceItems(signals, 0, (d, w) -> sel[0] = w) + .setPositiveButton(R.string.hugepage_proc_kill, + (d, w) -> doKill(proc, sel[0] == 0 ? SIGTERM : SIGKILL)) + .setNegativeButton(android.R.string.cancel, null) + .show(); + } + + private void doKill(@NonNull HugePageProcess proc, int signal) { + runOnPool(() -> { + var ok = shellKillProcess(proc.pid, signal); + runOnUiThread(() -> { + Toast.makeText(this, ok ? + R.string.hugepage_proc_kill_sent : + R.string.hugepage_proc_kill_failed, + LENGTH_SHORT).show(); + refresh(); + }); + }); + } + + @Override + public void onAcquire() { + if (acquireWatching) return; // already polling a run + Toast.makeText(this, R.string.hugepage_proc_acquire_running, LENGTH_SHORT).show(); + acquireWatching = true; + adapter.setAcquiring(true); // immediate spinner feedback + // Fire-and-poll on a dedicated thread so the shared pool executor (and + // thus the periodic refresh that animates the climbing numbers) is never + // blocked. The kernel 'acquire' write returns immediately; the worker + // migrates in the background and we watch acquire_active for completion. + new Thread(() -> { + boolean triggered = false; + try { + if (shellCheckExists(pathJoin(SYSFS_PARAMS, "acquire"))) { + triggered = run("echo 1 > %s/acquire", SYSFS_PARAMS).isSuccess(); + } else if (shellCheckExists(pathJoin(SYSFS_PARAMS, "manual_refill"))) { + triggered = run("echo 1 > %s/manual_refill", SYSFS_PARAMS).isSuccess(); + } + } catch (Exception e) { + Log.w(TAG, "acquire trigger failed", e); + } + if (!triggered) { + acquireWatching = false; + runOnUiThread(() -> Toast.makeText(this, + R.string.hugepage_refill_failed, LENGTH_SHORT).show()); + return; + } + // Poll until the worker clears acquire_active. Each tick nudges the + // UI to re-read refill_stat so the count visibly climbs. + long[] pages = null; + for (int i = 0; i < ACQUIRE_POLL_MAX; i++) { + try { Thread.sleep(ACQUIRE_POLL_MS); } catch (InterruptedException ignored) {} + pages = readPoolPages(); // {total, avail, want, acquiring, served} + runOnUiThread(this::refresh); // animate climbing numbers + if (pages == null || pages.length < 5 || pages[3] == 0) break; + } + var finalPages = pages; + acquireWatching = false; + runOnUiThread(() -> { + if (finalPages != null && finalPages.length >= 5) { + // Achieved = owned + traced (pool_avail + served), NOT + // pool_total: after a shrink that left served pages out, + // pool_total under-counts the pages VMs already hold. + long got = finalPages[1] + finalPages[4]; + long want = finalPages[2] >= 0 ? finalPages[2] : got; + String msg = got >= want + ? getString(R.string.hugepage_proc_acquire_full, + fmtPages(want)) + : getString(R.string.hugepage_proc_acquire_partial, + fmtPages(got), fmtPages(want)); + Toast.makeText(this, msg, LENGTH_LONG).show(); + } else { + Toast.makeText(this, R.string.hugepage_proc_acquire_done, + LENGTH_SHORT).show(); + } + refresh(); + }); + }, "hugepage-acquire").start(); + } + + /** Tapping the acquire spinner stops the run; the poll loop then finishes. */ + @Override + public void onStopAcquire() { + runOnPool(() -> { + try { + run("echo 0 > %s/acquire", SYSFS_PARAMS); + } catch (Exception e) { + Log.w(TAG, "stop acquire failed", e); + } + runOnUiThread(this::refresh); + }); + } + + /** Format a huge-page count (2 MiB each) as a human size, e.g. "5.84 GiB". */ + private static String fmtPages(long pages) { + return SizeUtils.formatSize(pages * HUGE_PAGE_BYTES); + } + + @Override + public void onShowStack(@NonNull HugePageProcess proc) { + runOnPool(() -> { + String stack; + try { + stack = shellReadFile(fmt("/proc/%d/stack", proc.pid)); + } catch (Exception e) { + stack = ""; + } + if (stack.trim().isEmpty()) + stack = getString(R.string.hugepage_proc_stack_unavailable); + var finalStack = stack; + runOnUiThread(() -> showStackDialog(proc, finalStack)); + }); + } + + private void showStackDialog(@NonNull HugePageProcess proc, @NonNull String stack) { + if (isFinishing()) return; + var tv = new TextView(this); + int pad = (int) (16 * getResources().getDisplayMetrics().density); + tv.setPadding(pad, pad, pad, pad); + tv.setTextIsSelectable(true); + tv.setTypeface(android.graphics.Typeface.MONOSPACE); + tv.setTextSize(12); + tv.setText(stack); + var scroll = new androidx.core.widget.NestedScrollView(this); + scroll.addView(tv); + new MaterialAlertDialogBuilder(this) + .setTitle(getString(R.string.hugepage_proc_stack_title, proc.pid)) + .setView(scroll) + .setPositiveButton(android.R.string.ok, null) + .show(); + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcessAdapter.java b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcessAdapter.java new file mode 100644 index 000000000..88aee65ac --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcessAdapter.java @@ -0,0 +1,198 @@ +package cn.classfun.droidvm.ui.hugepage; + +import static android.view.View.GONE; +import static android.view.View.VISIBLE; + +import android.annotation.SuppressLint; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.color.MaterialColors; + +import java.util.ArrayList; +import java.util.List; + +import cn.classfun.droidvm.R; +import cn.classfun.droidvm.lib.size.SizeUtils; + +@SuppressLint("NotifyDataSetChanged") +@SuppressWarnings("ClassEscapesDefinedScope") +public final class HugePageProcessAdapter + extends RecyclerView.Adapter { + + public interface Listener { + void onKill(@NonNull HugePageProcess proc); + + void onShowStack(@NonNull HugePageProcess proc); + + void onAcquire(); + + void onStopAcquire(); + } + + private static final long PAGE_SIZE = 2L * 1024 * 1024; + private final List items = new ArrayList<>(); + private final Listener listener; + private boolean acquiring = false; + + public HugePageProcessAdapter(@NonNull Listener listener) { + this.listener = listener; + setHasStableIds(true); + } + + /** Toggle the spinner on the Acquire row while a run is in flight. */ + public void setAcquiring(boolean acquiring) { + if (this.acquiring == acquiring) return; + this.acquiring = acquiring; + notifyDataSetChanged(); + } + + public void submit(@NonNull List newItems) { + items.clear(); + items.addAll(newItems); + notifyDataSetChanged(); + } + + @Override + public long getItemId(int position) { + return items.get(position).pid; + } + + @Override + public int getItemCount() { + return items.size(); + } + + @NonNull + @Override + public Holder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + var view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_hugepage_process, parent, false); + return new Holder(view); + } + + @Override + public void onBindViewHolder(@NonNull Holder holder, int position) { + var ctx = holder.itemView.getContext(); + var p = items.get(position); + + holder.icon.setColorFilter(p.color); + + // Usage line: page count (size). Always shown when known. + if (p.thpKb >= 0) { + holder.anon.setVisibility(VISIBLE); + holder.anon.setText(ctx.getString( + R.string.hugepage_proc_anon, + p.thpPages(), SizeUtils.formatSize(p.thpKb * 1024))); + } else { + holder.anon.setVisibility(GONE); + } + + if (p.unknown || p.acquire) { + // Synthetic rows: "unattributed" (no action) and "waiting for + // acquire" (deficit, with an Acquire button). + holder.title.setText(p.comm); + holder.served.setVisibility(GONE); + holder.stateView.setVisibility(GONE); + holder.btnStack.setVisibility(GONE); + holder.btnStack.setOnClickListener(null); + holder.itemView.setAlpha(1f); + if (p.acquire) { + // While a run is in flight show the spinner in place of the + // button - same look as before, but tapping it stops the acquire. + holder.progressAcquire.setVisibility(acquiring ? VISIBLE : GONE); + holder.progressAcquire.setOnClickListener( + acquiring ? v -> listener.onStopAcquire() : null); + holder.btnKill.setVisibility(acquiring ? GONE : VISIBLE); + holder.btnKill.setImageResource(R.drawable.ic_download); + holder.btnKill.setColorFilter(MaterialColors.getColor( + holder.itemView, androidx.appcompat.R.attr.colorPrimary)); + holder.btnKill.setContentDescription( + ctx.getString(R.string.hugepage_proc_acquire)); + holder.btnKill.setOnClickListener(v -> listener.onAcquire()); + } else { + holder.progressAcquire.setVisibility(GONE); + holder.progressAcquire.setOnClickListener(null); + holder.btnKill.setVisibility(GONE); + holder.btnKill.setOnClickListener(null); + } + return; + } + + holder.progressAcquire.setVisibility(GONE); + holder.btnKill.setVisibility(VISIBLE); + // Restore the kill affordance (views are recycled from acquire rows). + holder.btnKill.setImageResource(R.drawable.ic_close); + holder.btnKill.setColorFilter(MaterialColors.getColor( + holder.itemView, androidx.appcompat.R.attr.colorError)); + holder.btnKill.setContentDescription( + ctx.getString(R.string.hugepage_proc_kill)); + holder.title.setText(ctx.getString( + R.string.hugepage_proc_title, p.comm, p.pid)); + + // Served pages are only known for module-attributed owners; scan-sourced + // rows (servedPages < 0) hide it. + if (p.servedPages >= 0) { + holder.served.setVisibility(VISIBLE); + holder.served.setText(ctx.getString( + R.string.hugepage_proc_served, + p.servedPages, SizeUtils.formatSize(p.servedPages * PAGE_SIZE))); + } else { + holder.served.setVisibility(GONE); + } + + // Kernel state char only (R/S/D/...), shown left of the kill button. + // D (uninterruptible sleep) highlighted in the error color. + holder.stateView.setVisibility(VISIBLE); + holder.stateView.setText(String.valueOf(p.state)); + holder.stateView.setTextColor(MaterialColors.getColor(holder.itemView, + p.isUninterruptibleSleep() + ? androidx.appcompat.R.attr.colorError + : com.google.android.material.R.attr.colorOnSurfaceVariant)); + + boolean dead = !p.alive; + holder.itemView.setAlpha(dead ? 0.45f : 1f); + + // D-state -> show kernel-stack icon + if (p.isUninterruptibleSleep()) { + holder.btnStack.setVisibility(VISIBLE); + holder.btnStack.setOnClickListener(v -> listener.onShowStack(p)); + } else { + holder.btnStack.setVisibility(GONE); + holder.btnStack.setOnClickListener(null); + } + + holder.btnKill.setEnabled(!dead); + holder.btnKill.setOnClickListener(dead ? null : v -> listener.onKill(p)); + } + + static final class Holder extends RecyclerView.ViewHolder { + final ImageView icon; + final TextView title; + final TextView served; + final TextView anon; + final TextView stateView; + final ImageButton btnStack; + final ImageButton btnKill; + final View progressAcquire; + + Holder(@NonNull View v) { + super(v); + icon = v.findViewById(R.id.iv_proc_icon); + title = v.findViewById(R.id.tv_proc_title); + served = v.findViewById(R.id.tv_proc_served); + anon = v.findViewById(R.id.tv_proc_anon); + stateView = v.findViewById(R.id.tv_proc_state); + btnStack = v.findViewById(R.id.btn_proc_stack); + btnKill = v.findViewById(R.id.btn_proc_kill); + progressAcquire = v.findViewById(R.id.progress_proc_acquire); + } + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/SegmentedBar.java b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/SegmentedBar.java new file mode 100644 index 000000000..51d866ba3 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/SegmentedBar.java @@ -0,0 +1,77 @@ +package cn.classfun.droidvm.ui.hugepage; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; + +import cn.classfun.droidvm.R; + +/** + * A rounded horizontal bar split into colored segments (one per process) plus a + * track for the remaining/available portion. Widths are proportional; the caller + * passes a {@code total} that is always >= the sum of segment values, so the + * segments can never overflow the bar (no crash on over-accounting). + */ +public final class SegmentedBar extends View { + private int[] colors = new int[0]; + private float[] values = new float[0]; + private float total = 0f; + private int trackColor; + private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + private final RectF rect = new RectF(); + private final Path clip = new Path(); + private final float radius; + + public SegmentedBar(Context context) { + this(context, null); + } + + public SegmentedBar(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + radius = 3f * getResources().getDisplayMetrics().density; + trackColor = ContextCompat.getColor(context, R.color.hugepage_bar_track); + } + + public void setData(@NonNull int[] colors, @NonNull float[] values, float total) { + this.colors = colors; + this.values = values; + this.total = total; + invalidate(); + } + + @Override + protected void onDraw(@NonNull Canvas canvas) { + super.onDraw(canvas); + float w = getWidth(); + float h = getHeight(); + if (w <= 0 || h <= 0) return; + + rect.set(0, 0, w, h); + clip.reset(); + clip.addRoundRect(rect, radius, radius, Path.Direction.CW); + canvas.clipPath(clip); + + paint.setStyle(Paint.Style.FILL); + paint.setColor(trackColor); + canvas.drawRect(0, 0, w, h, paint); + + if (total <= 0f) return; + float x = 0f; + int n = Math.min(colors.length, values.length); + for (int i = 0; i < n; i++) { + float segW = w * (values[i] / total); + if (segW <= 0f) continue; + paint.setColor(colors[i]); + canvas.drawRect(x, 0, Math.min(x + segW, w), h, paint); + x += segW; + } + } +} diff --git a/app/src/main/res/drawable/ic_circle_filled.xml b/app/src/main/res/drawable/ic_circle_filled.xml new file mode 100644 index 000000000..862bd7269 --- /dev/null +++ b/app/src/main/res/drawable/ic_circle_filled.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_circle_outline.xml b/app/src/main/res/drawable/ic_circle_outline.xml new file mode 100644 index 000000000..add05b31d --- /dev/null +++ b/app/src/main/res/drawable/ic_circle_outline.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/layout/activity_hugepage.xml b/app/src/main/res/layout/activity_hugepage.xml index 79b609c00..8c3c9f8a4 100644 --- a/app/src/main/res/layout/activity_hugepage.xml +++ b/app/src/main/res/layout/activity_hugepage.xml @@ -1,24 +1,32 @@ - - + android:layout_height="wrap_content" + android:fitsSystemWindows="true"> + + + + + android:paddingBottom="16dp" + app:layout_behavior="@string/appbar_scrolling_view_behavior"> - + android:layout_weight="1"> + + + + + + + + + + + + android:text="@string/hugepage_btn_enable" + app:icon="@drawable/ic_start" /> - + android:layout_height="10dp" + android:layout_margin="12dp" /> @@ -180,15 +214,52 @@ android:layout_height="0dp" android:layout_weight="1" /> + + + + + + + + + + + + - + diff --git a/app/src/main/res/layout/activity_hugepage_process.xml b/app/src/main/res/layout/activity_hugepage_process.xml new file mode 100644 index 000000000..9e3d3c25f --- /dev/null +++ b/app/src/main/res/layout/activity_hugepage_process.xml @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_hugepage_process.xml b/app/src/main/res/layout/item_hugepage_process.xml new file mode 100644 index 000000000..e43c8f599 --- /dev/null +++ b/app/src/main/res/layout/item_hugepage_process.xml @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/menu/menu_hugepage_process.xml b/app/src/main/res/menu/menu_hugepage_process.xml new file mode 100644 index 000000000..706b70adc --- /dev/null +++ b/app/src/main/res/menu/menu_hugepage_process.xml @@ -0,0 +1,20 @@ + + + + + + + + + + diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index d1dcb16cd..aecd225e4 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -5,4 +5,6 @@ #FF3A1B1B #FF64B5F6 + + #80BFAF7C diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 57eaa15ee..149211848 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -910,6 +910,7 @@ 活动虚拟机 可用: %1$d (%2$s) 总共: %1$d (%2$s) + 池大小: %1$d (%2$s) 模块未加载 回收 回收已触发 @@ -943,4 +944,60 @@ 您的设备上的Gunyah版本过旧(如骁龙8 Gen 2),请尝试更新系统或更换新的设备。 沙箱模式不支持 crosvm的沙箱模式在此平台不可用,请禁用沙箱模式。 + + 模块已处于加载状态 + 停用 + 启用 + 安装 + 加载 + 加载模块失败 + 模块已加载 + 池大小不可小于运行中 VM 的内存总和(%1$s) + 获取中,无法缩小,请等待获取完成 + 池大小已保存并应用(增大需点获取) + 获取 + 获取完成 + 停止获取 + 获取完成:已达成 %1$s + 获取结束:达成 %1$s / 目标 %2$s(系统目前无法再凑出更多) + 获取大页中… + 模块不足,尝试 userspace 协助… + 占用:%1$d 页 (%2$s) + 等待获取 + 目前没有线程占用保留的 hugepage 池。 + 终止 + 发送信号失败 + 已发送信号 + 确定终止 %1$s? + 模块不可用,改用扫描模式 + 扫描中… + 显示模式 + 模块归属 (KO) + 系统扫描 (THP) + 未跟踪的虚拟机 + 内核模块未加载。 + 已加载的模块没有逐 VM 归属 API(旧版)。请从菜单切换系统扫描。 + 目前没有 VM 持有保留的大页。 + 目前没有运行中的 VM 在使用大页。 + 扫描模式 + 已分配:%1$d 页 (%2$s) + SIGKILL (9) — 强制 + SIGTERM (15) — 正常结束 + 内核堆栈 (pid %1$d) + 无法获取内核堆栈(线程可能已被唤醒)。 + 状态:%1$c (%2$s) + 不可中断睡眠 + 运行中 + 睡眠 + 已停止 + 未知 / 已结束 + 僵尸 + %1$s (pid %2$d) + Hugepage 池占用 + 未归属 + 未归属 — 已释放 %1$d / 已挪用 %2$d 页 + 虚拟机:%1$s + 保存 + 已使用: %1$d (%2$s) + 显示占用 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 8ff732ffe..c4bfb4a41 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -909,21 +909,78 @@ 已回收次數 活動虛擬機器 可用: %1$d (%2$s) + 已使用: %1$d (%2$s) 總共: %1$d (%2$s) + 池大小: %1$d (%2$s) 模組未載入 - 回收 - 回收已觸發 - 觸發回收失敗 + 獲取大頁 + 獲取已觸發 + 儲存 + 獲取 + 獲取大頁中… + 獲取完成 + 停止獲取 + 獲取完成:已達成 %1$s + 獲取結束:達成 %1$s / 目標 %2$s(系統目前無法再湊出更多) + 模組不足,嘗試 userspace 協助… + 觸發獲取失敗 池大小 池大小已儲存(重啟後生效) + 池大小已儲存並套用(放大需按獲取) + 獲取中,無法縮小,請待獲取完成 + 池大小不可小於執行中 VM 的記憶體總和(%1$s) 儲存池大小失敗 模組已啟用(重啟後生效) 模組已停用(重啟後生效) + 安裝 + 啟用 + 停用 + 載入 + 模組已載入 + 載入模組失敗 + 模組已在載入狀態 停止 確定要停止大頁預留模組嗎?這將釋放所有預分配的頁面,可能會影響正在執行的虛擬機器。 模組已解除安裝 解除安裝模組失敗 + 顯示占用 + Hugepage 集區占用 + %1$s (pid %2$d) + 已配發:%1$d 頁 (%2$s) + 占用:%1$d 頁 (%2$s) + 未歸屬 + 未歸屬 — 已釋放 %1$d / 已挪用 %2$d 頁 + 等待獲取 + 虛擬機器:%1$s + 非追蹤中的虛擬機器 + 狀態:%1$c (%2$s) + 執行中 + 睡眠 + 不可中斷睡眠 + 已停止 + 殭屍 + 未知 / 已結束 + 掃描模式 + 掃描中… + 顯示模式 + 模組歸屬 (KO) + 系統掃描 (THP) + 模組不可用,改用掃描模式 + 目前沒有執行緒占用保留的 hugepage 集區。 + 目前沒有 VM 持有保留的大頁。 + 目前沒有執行中的 VM 在使用大頁。 + 核心模組未載入。 + 已載入的模組沒有逐 VM 歸屬 API(舊版)。請從選單切換系統掃描。 + 終止 + 確定終止 %1$s? + SIGTERM (15) — 正常結束 + SIGKILL (9) — 強制 + 已送出訊號 + 送出訊號失敗 + 核心堆疊 (pid %1$d) + 無法取得核心堆疊(執行緒可能已被喚醒)。 + 允許通知? DroidVM 會在背景下載磁碟映像,並透過通知顯示進度。請允許通知,以便隨時查看下載進度。 繼續 diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 0f5cfb0fc..d0bb4de94 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -22,4 +22,7 @@ #FFFADFE0 #FF1976D2 + + #33808080 + #807D9CB3 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e6b5d8c90..8f86e2f1a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -946,21 +946,78 @@ Total Refilled Active VMs Available: %1$d (%2$s) + Used: %1$d (%2$s) Total: %1$d (%2$s) + Pool size: %1$d (%2$s) Module not loaded - Refill - Refill triggered - Failed to trigger refill + Acquire Pages + Acquire triggered + Failed to trigger acquire + Save + Acquire + Acquiring huge pages… + Acquire done + Stop acquiring + Acquire complete: reached %1$s + Acquire stopped: got %1$s of %2$s (system can\'t migrate more right now) + Module short; trying userspace assist… Pool Size Pool size saved (effective after reboot) + Pool size saved & applied (grow needs Acquire) + Acquiring — can\'t shrink now, wait for it to finish + Pool can\'t be smaller than the running VMs\' total memory (%1$s) Failed to save pool size Module enabled (effective after reboot) Module disabled (effective after reboot) + Install + Enable + Disable + Load + Module loaded + Failed to load module + Module already loaded Stop Are you sure you want to unload the hugepage reserve module? This will release all pre-allocated pages and may affect running VMs. Module unloaded Failed to unload module + Show Usage + Hugepage Pool Usage + %1$s (pid %2$d) + Served: %1$d pages (%2$s) + Usage: %1$d pages (%2$s) + Unattributed + Unattributed — freed %1$d / reused %2$d pages + Waiting for acquire + VM: %1$s + Not a tracked VM + State: %1$c (%2$s) + running + sleeping + uninterruptible sleep + stopped + zombie + unknown / exited + Scan mode + Scanning… + Display mode + Module attribution (KO) + System scan (THP) + Module unavailable — using scan mode + No processes are currently using the reserved hugepage pool. + No VM is holding reserved huge pages. + No running VM is using huge pages. + Kernel module is not loaded. + Loaded module has no per-VM attribution API (old build). Switch to System scan (menu). + Kill + Kill %1$s? + SIGTERM (15) — graceful + SIGKILL (9) — force + Signal sent + Failed to send signal + Kernel stack (pid %1$d) + Kernel stack unavailable (process may have woken up). + Allow notifications? DroidVM downloads disk images in the background and shows the progress in a notification. Allow notifications so you can keep an eye on the download. Continue From e2c82af04526ddb03bed4d94c7abbd1d070e409f Mon Sep 17 00:00:00 2001 From: lateautumn233 Date: Tue, 30 Jun 2026 01:20:59 +0800 Subject: [PATCH 071/182] feat(vm): add guest reboot support - Implement vm_reboot IPC command and RebootHandler - Add REBOOTING state and rebooting event in daemon-client communication - Support crosvm exit code detection (--extended-status flag) - Add CrosvmExit constants to distinguish reset from crash/panic/watchdog - Implement automatic VM relaunch on guest-requested reset (code 32) - Update console to track reboot state and show seamless relaunch - Update UI to use vm_reboot instead of stop+delay+start sequence - Add localized strings for reboot state and exit reasons --- app/src/main/cpp/console/commands/console.cpp | 24 +++++-- .../droidvm/daemon/ipc/vm/RebootHandler.java | 34 +++++++++ .../droidvm/daemon/vm/VMInstance.java | 69 ++++++++++++++++++- .../vm/backend/CrosvmBackendInstance.java | 3 + .../droidvm/lib/daemon/VMEventHandler.java | 25 ++++++- .../classfun/droidvm/lib/data/CrosvmExit.java | 32 +++++++++ .../droidvm/lib/store/vm/VMState.java | 4 ++ .../droidvm/ui/vm/info/VMInfoActivity.java | 8 ++- app/src/main/res/values-zh-rCN/strings.xml | 5 ++ app/src/main/res/values-zh-rTW/strings.xml | 1 + app/src/main/res/values/strings.xml | 5 ++ 11 files changed, 200 insertions(+), 10 deletions(-) create mode 100644 app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/RebootHandler.java create mode 100644 app/src/main/java/cn/classfun/droidvm/lib/data/CrosvmExit.java diff --git a/app/src/main/cpp/console/commands/console.cpp b/app/src/main/cpp/console/commands/console.cpp index 99647b758..817d5d318 100644 --- a/app/src/main/cpp/console/commands/console.cpp +++ b/app/src/main/cpp/console/commands/console.cpp @@ -43,6 +43,7 @@ class ConsoleCommand : public Command { ); bool running = false; + bool rebooting = false; }; void ConsoleCommand::console_list(const std::string &vm) { @@ -94,12 +95,25 @@ void ConsoleCommand::process_event( fflush(stdout); } } - } else if (event == "exited" || event == "state") { + } else if (event == "rebooting") { + // Guest-initiated reboot: the daemon tears down crosvm and relaunches it + // reusing the same console streams. Stay attached so the new boot's output + // streams in seamlessly instead of dropping the terminal. + rebooting = true; + fprintf(stderr, "\n[droidvm] VM rebooting...\n"); + } else if (event == "exited") { + // The only authoritative terminal signal. Every non-reboot end path fires + // "exited" (with the real code); a bare "stopped" state during reboot does not. + int code = edata.get("exit_code", -1).asInt(); + fprintf(stderr, "\n[droidvm] VM exited (code %d).\n", code); + running = false; + } else if (event == "state") { auto state = edata.get("state", "").asString(); - if (state == "stopped") { - int code = edata.get("exit_code", -1).asInt(); - fprintf(stderr, "\n[droidvm] VM exited (code %d).\n", code); - running = false; + // A "stopped" state event arrives during reboot too, so it is NOT terminal + // on its own - we wait for "exited". Just clear the reboot notice once back up. + if (state == "running" && rebooting) { + rebooting = false; + fprintf(stderr, "[droidvm] VM is back up.\n"); } } } diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/RebootHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/RebootHandler.java new file mode 100644 index 000000000..fb3af10a8 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/RebootHandler.java @@ -0,0 +1,34 @@ +package cn.classfun.droidvm.daemon.ipc.vm; + +import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; + +import androidx.annotation.NonNull; + +import com.google.auto.service.AutoService; + +import cn.classfun.droidvm.daemon.server.ClientRequest; +import cn.classfun.droidvm.daemon.server.RequestException; +import cn.classfun.droidvm.daemon.server.RequestHandler; + +@AutoService(RequestHandler.class) +public final class RebootHandler extends RequestHandler { + @NonNull + @Override + public String getName() { + return "vm_reboot"; + } + + @Override + public void handle(@NonNull ClientRequest request) throws Exception { + var params = request.getParams(); + var vmId = params.optString("vm_id", ""); + if (vmId.isEmpty()) + throw new RequestException("missing vm_id"); + var vms = request.getContext().getVMs(); + var inst = vms.findById(vmId); + if (inst == null) + throw new RequestException(fmt("VM not found: %s", vmId)); + if (!inst.reboot()) + throw new RequestException("failed to reboot VM"); + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMInstance.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMInstance.java index 2a71e0893..c69030ff5 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMInstance.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMInstance.java @@ -23,6 +23,7 @@ import cn.classfun.droidvm.daemon.console.ConsoleStream; import cn.classfun.droidvm.daemon.vm.backend.BackendBase; +import cn.classfun.droidvm.lib.data.CrosvmExit; import cn.classfun.droidvm.lib.natives.NativeProcess; import cn.classfun.droidvm.lib.natives.UnixHelper; import cn.classfun.droidvm.lib.store.base.DataItem; @@ -45,6 +46,11 @@ public final class VMInstance extends VMConfig { private final VMInstanceStore store; private BootPlan bootPlan; private String bootEntryOverride; + private volatile boolean rebootRequested = false; + + // Delay before relaunching on reboot: lets the kernel settle same-name TAP + // teardown/recreate and throttles a guest reboot-loop (no restart cap). + private static final long REBOOT_RELAUNCH_DELAY_MS = 500; public interface VMEventCallback { @SuppressWarnings("unused") @@ -148,7 +154,9 @@ public void setBootEntryOverride(@Nullable String entryId) { @SuppressWarnings("BooleanMethodIsAlwaysInverted") public boolean start() { - if (state != VMState.STOPPED) { + // REBOOTING is accepted too: the reboot relaunch calls start() from that + // transient state (process already gone) and goes straight to STARTING. + if (state != VMState.STOPPED && state != VMState.REBOOTING) { Log.w(TAG, fmt("VM %s is not stopped (state=%s), cannot start", getId(), state.name())); return false; } @@ -277,6 +285,26 @@ public boolean stop() { return true; } + public boolean reboot() { + if (state != VMState.RUNNING) { + Log.w(TAG, fmt("Cannot reboot VM %s: state=%s", getId(), state.name())); + return false; + } + Log.i(TAG, fmt("Reboot requested for VM %s", getName())); + // crosvm has no reset control command, so reboot = ask crosvm to exit and + // let runVM() relaunch via the rebootRequested flag (kept set on fallback). + rebootRequested = true; + if (getBackendInstance().hasControlSocket()) { + if (runControlCommand("stop")) return true; + Log.w(TAG, fmt("Control stop failed for reboot of VM %s, falling back to destroy", getName())); + } + if (process != null && process.isAlive()) { + process.destroy(); + shellKillProcess(process.pid()); + } + return true; + } + public boolean suspend() { if (state != VMState.RUNNING) { Log.w(TAG, fmt("Cannot suspend VM %s: state=%s", getId(), state.name())); @@ -399,6 +427,9 @@ private void runVM() { } catch (Exception ignored) { } } + // A guest-requested reset (crosvm exit 32) or a host-issued reboot both + // relaunch the VM; an explicit user stop never does and wins over both. + boolean wantRestart = !stoppedByUser && (code == CrosvmExit.RESET.getCode() || rebootRequested); if (stoppedByUser && code != 0) { Log.i(TAG, fmt("VM %s stopped by user", getName())); exitCode = 0; @@ -407,10 +438,46 @@ private void runVM() { } cleanupTap(); inst.cleanup(); + if (wantRestart) { + rebootRequested = false; + exitCode = -1; + // Broadcast REBOOTING (not STOPPED) so clients render "rebooting" for the + // whole relaunch window instead of flickering to a stopped row/button. + // start() accepts REBOOTING, so the next transition is straight to STARTING. + setState(VMState.REBOOTING); + fireEvent("rebooting", null); + Log.i(TAG, fmt("VM %s rebooting (exit code %d)", getName(), code)); + scheduleRelaunch(); + return; + } setState(VMState.STOPPED); fireEvent("exited", null); } + // Relaunch off the worker thread: start() joins workerThread (this thread), + // and the short sleep settles same-name TAP recreation before re-setup. + private void scheduleRelaunch() { + var t = new Thread(() -> { + try { + Thread.sleep(REBOOT_RELAUNCH_DELAY_MS); + } catch (InterruptedException ignored) { + return; + } + if (state != VMState.REBOOTING) return; // user changed state meanwhile + if (!start()) { + Log.w(TAG, fmt("VM %s relaunch failed", getName())); + // Surface the failure as a real exit so attached consoles / UI stop + // waiting for the reboot to come back. start() left us in REBOOTING, + // so settle to STOPPED before the exit fires the correct final state. + exitCode = -1; + setState(VMState.STOPPED); + fireEvent("exited", null); + } + }, fmt("VM-relaunch-%s", getId())); + t.setDaemon(true); + t.start(); + } + private void cleanupTap() { var nets = requireNonNull(item.opt("networks", DataItem.newArray())); if (nets.isEmpty()) return; diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackendInstance.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackendInstance.java index 7a4e9a566..f83505f23 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackendInstance.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackendInstance.java @@ -136,6 +136,9 @@ private List buildCommand() { var args = new ArrayList(); prepareStraceArguments(args); args.add(getPrebuiltBinaryPath("crosvm")); + // Top-level flag (must precede `run`): makes crosvm surface CommandStatus + // exit codes (see CrosvmExit) so runVM() can tell reset/crash/panic apart. + args.add("--extended-status"); args.add("run"); args.add("--name"); args.add(config.getName()); diff --git a/app/src/main/java/cn/classfun/droidvm/lib/daemon/VMEventHandler.java b/app/src/main/java/cn/classfun/droidvm/lib/daemon/VMEventHandler.java index 855be845f..26788ee84 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/daemon/VMEventHandler.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/daemon/VMEventHandler.java @@ -36,6 +36,7 @@ import java.util.UUID; import cn.classfun.droidvm.R; +import cn.classfun.droidvm.lib.data.CrosvmExit; import cn.classfun.droidvm.lib.diag.LogHelper; import cn.classfun.droidvm.lib.store.vm.VMState; import cn.classfun.droidvm.lib.store.vm.VMStore; @@ -92,6 +93,24 @@ private void callOnVmExited(UUID vmId, String vmName, int exitCode, JSONObject d c.onVMExited(vmId, vmName, exitCode, data); } + /** Maps a crosvm exit code to a user-facing message resource (args: vmName, code). */ + public static int exitMessageRes(int exitCode) { + CrosvmExit exit = CrosvmExit.fromCode(exitCode); + if (exit == null) { + return R.string.vm_exited_error; + } + switch (exit) { + case CRASH: + return R.string.vm_exit_crash; + case GUEST_PANIC: + return R.string.vm_exit_guest_panic; + case WATCHDOG: + return R.string.vm_exit_watchdog; + default: + return R.string.vm_exited_error; + } + } + private void showExitDialog(Activity act, String vmName, int exitCode, @NonNull JSONObject data) { var logText = new StringBuilder(); var stdio = data.optString("stdio", ""); @@ -140,7 +159,7 @@ public void onVMExited(UUID vmId, String vmName, int exitCode, JSONObject data) } else { Toast.makeText( act, - act.getString(R.string.vm_exited_error, vmName, exitCode), + act.getString(exitMessageRes(exitCode), vmName, exitCode), LENGTH_LONG ).show(); showExitDialog(act, vmName, exitCode, data); @@ -169,6 +188,10 @@ public void onDaemonEvent(@NonNull JSONObject msg) { if (foregroundCallback.isEmpty() || !isAppInForeground()) showExitNotification(vmId, vmName, exitCode); callOnVmExited(vmId, vmName, exitCode, data); + } else if (event.equals("rebooting")) { + queueActivityTask(act -> Toast.makeText( + act, act.getString(R.string.vm_rebooting, vmName), LENGTH_SHORT + ).show(), SplashActivity.class); } }); } diff --git a/app/src/main/java/cn/classfun/droidvm/lib/data/CrosvmExit.java b/app/src/main/java/cn/classfun/droidvm/lib/data/CrosvmExit.java new file mode 100644 index 000000000..db4920b34 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/lib/data/CrosvmExit.java @@ -0,0 +1,32 @@ +package cn.classfun.droidvm.lib.data; + +/** + * crosvm process exit codes (CommandStatus). RESET drives the automatic relaunch + * in VMInstance; the rest are surfaced to the user as distinct failure reasons. + */ +public enum CrosvmExit { + RESET(32), // VmReset: guest requested reboot/reset + CRASH(33), // VmCrash + GUEST_PANIC(34), // kernel panic in guest + WATCHDOG(36); // vcpu stall / watchdog reset + + private final int code; + + CrosvmExit(int code) { + this.code = code; + } + + public int getCode() { + return code; + } + + /** Maps a raw crosvm exit code to its constant, or null when unrecognized. */ + public static CrosvmExit fromCode(int code) { + for (CrosvmExit exit : values()) { + if (exit.code == code) { + return exit; + } + } + return null; + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMState.java b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMState.java index e53500d5e..291ff6859 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMState.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMState.java @@ -27,6 +27,10 @@ public enum VMState implements StringEnum, ColorEnum { STOPPING( R.string.vm_state_stopping, android.R.color.holo_orange_dark + ), + REBOOTING( + R.string.vm_state_rebooting, + android.R.color.holo_orange_dark ); private final @StringRes int stringId; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/info/VMInfoActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/info/VMInfoActivity.java index ff994359a..fac636af7 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/info/VMInfoActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/info/VMInfoActivity.java @@ -44,6 +44,7 @@ import cn.classfun.droidvm.R; import cn.classfun.droidvm.lib.daemon.DaemonConnection; import cn.classfun.droidvm.lib.daemon.ForegroundCallback; +import cn.classfun.droidvm.lib.daemon.VMEventHandler; import cn.classfun.droidvm.lib.store.base.DataItem; import cn.classfun.droidvm.lib.store.network.NetworkStore; import cn.classfun.droidvm.lib.store.vm.BootConfig; @@ -204,7 +205,7 @@ public void onVMExited(UUID id, String vmName, int exitCode, JSONObject data) { ).show(); } else { Toast.makeText(this, - getString(R.string.vm_exited_error, vmName, exitCode), + getString(VMEventHandler.exitMessageRes(exitCode), vmName, exitCode), LENGTH_LONG ).show(); } @@ -431,9 +432,10 @@ private void doStop() { } private void doRestart() { + // crosvm reboot: daemon stops crosvm and relaunches it (no client-side + // stop+delay+start); a guest-initiated reboot takes the same daemon path. DialogInterface.OnClickListener cb = (d, w) -> - sendCommand("vm_stop", vmId, mainHandler, ui, - () -> mainHandler.postDelayed(this::doStart, 1500)); + sendCommand("vm_reboot", vmId, mainHandler, ui); new MaterialAlertDialogBuilder(this) .setTitle(config.getName()) .setMessage(R.string.vm_info_restart_confirm) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 57eaa15ee..5d6581518 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -107,9 +107,14 @@ 启动中 运行中 停止中 + 重启中 虚拟机"%1$s"已停止。 虚拟机"%1$s"以退出码 %2$d 退出。 + 虚拟机"%1$s"已崩溃(退出码 %2$d)。 + 虚拟机"%1$s"guest 内核 panic(退出码 %2$d)。 + 虚拟机"%1$s"卡死,已被看门狗复位(退出码 %2$d)。 + 正在重启虚拟机"%1$s"… 虚拟机"%1$s"启动失败 虚拟机以退出码 %1$d 退出。\n\n日志: 无可用日志。 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 8ff732ffe..38a2d43cf 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -107,6 +107,7 @@ 啟動中 執行中 停止中 + 重新啟動中 虛擬機器"%1$s"已停止。 虛擬機器"%1$s"以退出碼 %2$d 退出。 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e6b5d8c90..4375bd783 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -108,9 +108,14 @@ Starting Running Stopping + Rebooting VM \"%1$s\" has stopped. VM \"%1$s\" exited with code %2$d. + VM \"%1$s\" crashed (code %2$d). + VM \"%1$s\" hit a kernel panic in the guest (code %2$d). + VM \"%1$s\" stalled and was reset by the watchdog (code %2$d). + Rebooting VM \"%1$s\"… VM \"%1$s\" Failed The virtual machine exited with code %1$d.\n\nLogs: No logs available. From b5f697f623b53392d07a3baa7a5fef8466042628 Mon Sep 17 00:00:00 2001 From: BigfootACA Date: Sun, 28 Jun 2026 09:44:43 +0800 Subject: [PATCH 072/182] StringUtils: resolveUriPath: add document parse Signed-off-by: BigfootACA --- .../cn/classfun/droidvm/lib/utils/StringUtils.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/cn/classfun/droidvm/lib/utils/StringUtils.java b/app/src/main/java/cn/classfun/droidvm/lib/utils/StringUtils.java index 879cffdc0..f1ab98d08 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/utils/StringUtils.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/utils/StringUtils.java @@ -72,6 +72,13 @@ private static String parseTreePath(@Nullable String content, @Nullable String t if (shellCheckExists(content)) return content; var real = pathJoin(extStorage, content); if (shellCheckExists(real)) return real; + int slash = content.indexOf("/document/"); + if (slash >= 0) { + content = content.substring(0, slash); + if (shellCheckExists(content)) return content; + real = pathJoin(extStorage, content); + if (shellCheckExists(real)) return real; + } return null; } @@ -96,12 +103,6 @@ public static String resolveUriPath(Context ctx, Uri uri) { if ((part = parseTreePath(path, "/document/primary:")) != null) return part; if ((part = parseTreePath(path, "/external_files/")) != null) return part; if ((part = parseTreePath(path, "/tree/primary:")) != null) return part; - if (path != null && path.startsWith("/tree/primary:")) { - var sub = path.substring("/tree/primary:".length()); - int slash = sub.indexOf("/document/"); - if (slash >= 0) sub = sub.substring(0, slash); - return pathJoin(externalPath(), sub); - } return path; } From e0d512ee0bed3e8a29dbf3872568a771da2f4fb0 Mon Sep 17 00:00:00 2001 From: BigfootACA Date: Sun, 28 Jun 2026 09:45:16 +0800 Subject: [PATCH 073/182] DiskInfoInfoTab: auto close when disk deleted Signed-off-by: BigfootACA --- .../ui/disk/info/info/DiskInfoInfoTab.java | 36 +++++++++++++------ 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/cn/classfun/droidvm/ui/disk/info/info/DiskInfoInfoTab.java b/app/src/main/java/cn/classfun/droidvm/ui/disk/info/info/DiskInfoInfoTab.java index 0423d740d..f819a5845 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/disk/info/info/DiskInfoInfoTab.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/disk/info/info/DiskInfoInfoTab.java @@ -8,6 +8,7 @@ import android.content.ClipboardManager; import android.content.Context; import android.content.DialogInterface; +import android.util.Log; import android.widget.FrameLayout; import android.widget.TextView; @@ -22,6 +23,7 @@ import cn.classfun.droidvm.R; import cn.classfun.droidvm.lib.store.disk.DiskConfig; +import cn.classfun.droidvm.lib.store.disk.DiskStore; import cn.classfun.droidvm.ui.disk.action.DiskActionDialog; import cn.classfun.droidvm.ui.disk.info.DiskInfoActivity; import cn.classfun.droidvm.ui.disk.info.base.DiskInfoBaseTab; @@ -29,6 +31,7 @@ import cn.classfun.droidvm.ui.widgets.row.TextRowWidget; public final class DiskInfoInfoTab extends DiskInfoBaseTab { + private static final String TAG = "DiskInfoInfoTab"; private CollapsibleContainer sectionDetails; private CollapsibleContainer sectionRaw; private TextView tvSummary; @@ -91,23 +94,23 @@ public void onCreateView() { } private void initialize() { - dialog = new DiskActionDialog(activity, null, null); + dialog = new DiskActionDialog(activity, this::onDiskUpdated, null); bindButton(btnResize, R.id.menu_disk_resize); bindButton(btnConvert, R.id.menu_disk_convert); bindButton(btnOptimize, R.id.menu_disk_optimize); bindButton(btnCreateIncrement, R.id.menu_disk_create_increment); bindButton(btnClone, R.id.menu_disk_clone); bindButton(btnDelete, R.id.menu_disk_delete); - bindCopy(rowFilename); - bindCopy(rowFolder); - bindCopy(rowFormat); - bindCopy(rowVirtualSize); - bindCopy(rowActualSize); - bindCopy(rowClusterSize); - bindCopy(rowBackingFile); - bindCopy(rowEncryption); - bindCopy(rowCompression); - bindCopy(rowDirtyFlag); + bindCopy(rowFilename); + bindCopy(rowFolder); + bindCopy(rowFormat); + bindCopy(rowVirtualSize); + bindCopy(rowActualSize); + bindCopy(rowClusterSize); + bindCopy(rowBackingFile); + bindCopy(rowEncryption); + bindCopy(rowCompression); + bindCopy(rowDirtyFlag); var rowExtra1 = view.findViewById(R.id.row_extra_1); var rowExtra2 = view.findViewById(R.id.row_extra_2); var fmt = activity.config.getFormat(); @@ -117,6 +120,17 @@ private void initialize() { } } + private void onDiskUpdated() { + try { + var store = new DiskStore(); + store.load(activity); + if (store.findById(activity.config.getId()) == null) + activity.runOnUiThread(activity::finish); + } catch (Exception e) { + Log.w(TAG, "Failed to load disk info", e); + } + } + private void bindButton(@NonNull MaterialButton btn, @IdRes int id) { btn.setOnClickListener(v -> dialog.diskMenuOnClick(activity.config, id)); } From 4012a47def07cc500592da69e01b79b7fa1c9ffe Mon Sep 17 00:00:00 2001 From: BigfootACA Date: Sun, 28 Jun 2026 09:52:35 +0800 Subject: [PATCH 074/182] VMEditGraphicsTab: add checks for native display Signed-off-by: BigfootACA --- .../classfun/droidvm/ui/vm/edit/VMEditActivity.java | 13 +++++++++++++ .../droidvm/ui/vm/edit/basic/VMEditBasicTab.java | 4 ++++ .../ui/vm/edit/graphics/VMEditGraphicsTab.java | 5 +++++ app/src/main/res/values-zh-rCN/strings.xml | 2 ++ app/src/main/res/values-zh-rTW/strings.xml | 2 ++ app/src/main/res/values/strings.xml | 2 ++ 6 files changed, 28 insertions(+) diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/VMEditActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/VMEditActivity.java index 835b5f21e..acb06d0f9 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/VMEditActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/VMEditActivity.java @@ -19,7 +19,9 @@ import com.google.android.material.tabs.TabLayout; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.UUID; import java.util.function.Consumer; @@ -45,6 +47,17 @@ public final class VMEditActivity extends SwipeableTabActivity { public Consumer currentPicker = null; public boolean editMode = false; public UUID editVMId = null; + private final Map sharedData = new HashMap<>(); + + + public void put(@NonNull String key, T value) { + sharedData.put(key, value); + } + + @SuppressWarnings("unchecked") + public T get(@NonNull String key, T def) { + return (T) sharedData.getOrDefault(key, def); + } private void pickerResult(Uri uri) { if (currentPicker != null && uri != null) diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/basic/VMEditBasicTab.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/basic/VMEditBasicTab.java index f6d8eeebe..4d5f4173b 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/basic/VMEditBasicTab.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/basic/VMEditBasicTab.java @@ -86,6 +86,10 @@ public void initValue() { chooseBackend.configure(VMBackend.class, VMBackend.DEFAULT); chooseHypervisor.configure(VMHypervisor.class, VMHypervisor.DEFAULT); choosePrepareLendMthp.configure(LendMthpMode.class, LendMthpMode.CHUNKED); + parent.put("backend", VMBackend.DEFAULT); + parent.put("hypervisor", VMHypervisor.DEFAULT); + chooseBackend.setOnValueChangedListener((oldValue, newValue) -> parent.put("backend", newValue)); + chooseHypervisor.setOnValueChangedListener((oldValue, newValue) -> parent.put("hypervisor", newValue)); try { var socModel = QcomChipName.getCurrentSoC(); var gunyah = new QcomGunyahSupports(parent); diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/graphics/VMEditGraphicsTab.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/graphics/VMEditGraphicsTab.java index 579c4caa4..3b8d9e6ee 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/graphics/VMEditGraphicsTab.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/graphics/VMEditGraphicsTab.java @@ -17,6 +17,7 @@ import com.google.android.material.textfield.TextInputEditText; import cn.classfun.droidvm.R; +import cn.classfun.droidvm.lib.store.vm.VMBackend; import cn.classfun.droidvm.lib.store.vm.VMConfig; import cn.classfun.droidvm.lib.store.vm.VMStore; import cn.classfun.droidvm.lib.store.vm.DisplayBackend; @@ -171,6 +172,10 @@ public boolean validateInput(@NonNull VMStore store) { if (!checkInputField(etDisplayDpiH, false, 100, 800)) return false; if (!checkInputField(etDisplayDpiV, false, 100, 800)) return false; if (!checkInputField(etVncPort, true, 1024, 65535)) return false; + if (parent.get("backend", VMBackend.DEFAULT) != VMBackend.CROSVM && swNativeDisplayEnabled.isChecked()) + return showValidateFailed(R.string.create_vm_error_native_display_only_crosvm); + if (swNativeDisplayEnabled.isChecked() && swVncEnabled.isChecked()) + return showValidateFailed(R.string.create_vm_error_native_display_vnc_cannot_enabled); return true; } diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 1b2cf1bb0..d9251fda3 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -173,6 +173,8 @@ 无效数字 所选后端不支持选中的虚拟化模式 无效的绝对路径 + 原生显示只在使用CrosVM后端时有效 + 原生显示和VNC不能同时启用 Linux 内核 内核 Initrd diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 446626b2b..04983f8ad 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -169,6 +169,8 @@ 無效數字 所選後端不支援選取的虛擬化模式 無效的絕對路徑 + 原生顯示只在使用CrosVM後端時有效 + 原生顯示和VNC不能同時啟用 Linux 核心 核心 Initrd diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 91b4d5a27..e43ba330b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -175,6 +175,8 @@ Invalid number Selected hypervisor is not supported by selected backend Invalid absolute path + Native display only applies to CrosVM backend. + Native display and VNC cannot be enabled at the same time. Linux Kernel Kernel Initrd From 711168c61eebcba7a9431664f3c6a4c1fbf574d9 Mon Sep 17 00:00:00 2001 From: BigfootACA Date: Mon, 29 Jun 2026 08:02:43 +0800 Subject: [PATCH 075/182] license.yaml: add new dependencies Signed-off-by: BigfootACA --- app/src/main/assets/data/license.yaml | 34 +++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/app/src/main/assets/data/license.yaml b/app/src/main/assets/data/license.yaml index 00517d89d..693ce239f 100644 --- a/app/src/main/assets/data/license.yaml +++ b/app/src/main/assets/data/license.yaml @@ -104,3 +104,37 @@ license: urls: - https://android.googlesource.com/platform/hardware/google/gfxstream/ - https://github.com/google/gfxstream + + - name: netbox + description: Network manage utility for DroidVM + license: GPL-3.0 + urls: + - https://github.com/Droid-VM/netbox + + - name: gvisor-vswitch + description: Virtual Switch based on gVisor + license: Apache-2.0 + urls: + - https://github.com/Droid-VM/gvisor-vswitch + - https://github.com/HuJK/gvisor-vswitch + + - name: pseudo-bridge-rs + description: Pseudo Bridge + license: Apache-2.0 + urls: + - https://github.com/Droid-VM/pseudo-bridge-rs + - https://github.com/KusakabeShi/pseudo-bridge-rs + + - name: linux-boot-extractor + description: Linux Boot Extractor + license: GPL-3.0 + urls: + - https://github.com/Droid-VM/linux-boot-extractor + - https://github.com/HuJK/linux-boot-extractor + + - name: bridgedhcp + description: DHCPv4 / DHCPv6 / SLAAC server for Linux bridge interfaces + license: GPL-3.0 + urls: + - https://github.com/Droid-VM/bridgedhcp + - https://github.com/HuJK/bridgedhcp From e78b27577e51a9b09f6c2b3a4d5bba39b78b4373 Mon Sep 17 00:00:00 2001 From: BigfootACA Date: Mon, 29 Jun 2026 08:04:43 +0800 Subject: [PATCH 076/182] DiskAdapter: display cdrom icon on ISO files Signed-off-by: BigfootACA --- .../cn/classfun/droidvm/ui/main/base/list/DataAdapter.java | 4 ++-- .../java/cn/classfun/droidvm/ui/main/disk/DiskAdapter.java | 5 ++++- .../cn/classfun/droidvm/ui/main/network/NetworkAdapter.java | 2 +- .../main/java/cn/classfun/droidvm/ui/main/vm/VMAdapter.java | 2 +- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/cn/classfun/droidvm/ui/main/base/list/DataAdapter.java b/app/src/main/java/cn/classfun/droidvm/ui/main/base/list/DataAdapter.java index 54d833e27..4aa236459 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/main/base/list/DataAdapter.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/main/base/list/DataAdapter.java @@ -55,7 +55,7 @@ public final BaseViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int vi public void onBindViewHolder(@NonNull BaseViewHolder h, int position) { var d = items.get(position); var ctx = h.itemView.getContext(); - var drawable = AppCompatResources.getDrawable(ctx, getIconResId()); + var drawable = AppCompatResources.getDrawable(ctx, getIconResId(d)); h.itemIcon.setImageDrawable(drawable); if (d != null) h.itemName.setText(d.getName()); @@ -71,7 +71,7 @@ public void onBindViewHolder(@NonNull BaseViewHolder h, int position) { } @DrawableRes - public abstract int getIconResId(); + public abstract int getIconResId(@NonNull D data); @SuppressLint("NotifyDataSetChanged") public void onItemsUpdated() { diff --git a/app/src/main/java/cn/classfun/droidvm/ui/main/disk/DiskAdapter.java b/app/src/main/java/cn/classfun/droidvm/ui/main/disk/DiskAdapter.java index f6ad2dbdd..e97e932b4 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/main/disk/DiskAdapter.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/main/disk/DiskAdapter.java @@ -18,6 +18,7 @@ import cn.classfun.droidvm.lib.store.disk.DiskConfig; import cn.classfun.droidvm.lib.store.disk.DiskStore; import cn.classfun.droidvm.lib.utils.ImageUtils; +import cn.classfun.droidvm.ui.disk.create.DiskFormat; import cn.classfun.droidvm.ui.main.base.BaseViewHolder; import cn.classfun.droidvm.ui.main.base.list.DataAdapter; @@ -36,7 +37,9 @@ public void onItemsUpdated() { } @Override - public int getIconResId() { + public int getIconResId(@NonNull DiskConfig disk) { + if (disk.getFormat() == DiskFormat.ISO) + return R.drawable.ic_cdrom; return R.drawable.ic_nav_disk; } diff --git a/app/src/main/java/cn/classfun/droidvm/ui/main/network/NetworkAdapter.java b/app/src/main/java/cn/classfun/droidvm/ui/main/network/NetworkAdapter.java index b1230ca09..5aeb08da1 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/main/network/NetworkAdapter.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/main/network/NetworkAdapter.java @@ -18,7 +18,7 @@ public NetworkAdapter() { } @Override - public int getIconResId() { + public int getIconResId(@NonNull NetworkConfig network) { return R.drawable.ic_switch; } diff --git a/app/src/main/java/cn/classfun/droidvm/ui/main/vm/VMAdapter.java b/app/src/main/java/cn/classfun/droidvm/ui/main/vm/VMAdapter.java index a32f42bde..29a66d326 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/main/vm/VMAdapter.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/main/vm/VMAdapter.java @@ -17,7 +17,7 @@ public VMAdapter() { } @Override - public int getIconResId() { + public int getIconResId(@NonNull VMConfig vm) { return R.drawable.ic_nav_vm; } From c7fbaec8ebbeef68a957893e4dedf46ddf4a49c4 Mon Sep 17 00:00:00 2001 From: BigfootACA Date: Tue, 30 Jun 2026 17:13:34 +0800 Subject: [PATCH 077/182] assets: update prebuilts https://github.com/Droid-VM/DroidVM-Prebuilt-Root/commit/4016dba63af0970337eadde0eb1d810c15a048e2 Signed-off-by: BigfootACA --- app/src/main/assets/prebuilts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/assets/prebuilts b/app/src/main/assets/prebuilts index 06a1cc4e4..fc972f187 160000 --- a/app/src/main/assets/prebuilts +++ b/app/src/main/assets/prebuilts @@ -1 +1 @@ -Subproject commit 06a1cc4e47be68d030aa54546175c8172f2786a0 +Subproject commit fc972f1871a2accb31fc0d5f2c08cc8a37d8dd8a From 4e0d2682458930e86c40027e51edb654ee581806 Mon Sep 17 00:00:00 2001 From: BigfootACA Date: Tue, 30 Jun 2026 17:17:05 +0800 Subject: [PATCH 078/182] qcom.xml: add SM8735P Signed-off-by: BigfootACA --- app/src/main/assets/data/gunyah.yaml | 1 + app/src/main/res/xml/qcom.xml | 1 + 2 files changed, 2 insertions(+) diff --git a/app/src/main/assets/data/gunyah.yaml b/app/src/main/assets/data/gunyah.yaml index 9886f2963..7738ddc1a 100644 --- a/app/src/main/assets/data/gunyah.yaml +++ b/app/src/main/assets/data/gunyah.yaml @@ -16,6 +16,7 @@ gunyah: # Qualcomm Snapdragon 8 Elite - socs: - SM8735 + - SM8735P - SM8750 - SM87503AB - SM8750AB diff --git a/app/src/main/res/xml/qcom.xml b/app/src/main/res/xml/qcom.xml index de1d2c360..c111404df 100644 --- a/app/src/main/res/xml/qcom.xml +++ b/app/src/main/res/xml/qcom.xml @@ -212,6 +212,7 @@ Qualcomm Snapdragon 8 Gen 3 Qualcomm Snapdragon 8 Gen 3 Qualcomm Snapdragon 8s Gen 4 + Qualcomm Snapdragon 8s Gen 4 Qualcomm Snapdragon 8 Elite Qualcomm Snapdragon 8 Elite Qualcomm Snapdragon 8 Elite From ea3b38bcc490d1fee6ce300904627332b2cb26d6 Mon Sep 17 00:00:00 2001 From: HuJK Date: Tue, 30 Jun 2026 18:33:39 +0800 Subject: [PATCH 079/182] (feat) UI update usage bar --- .../droidvm/ui/hugepage/HugePageActivity.java | 106 +++-- .../droidvm/ui/hugepage/HugePageModel.java | 376 ++++++++++++++++++ .../ui/hugepage/HugePageProcessActivity.java | 289 ++++---------- .../droidvm/ui/hugepage/SegmentedBar.java | 170 +++++++- app/src/main/res/layout/activity_hugepage.xml | 2 +- app/src/main/res/values-zh-rCN/strings.xml | 4 +- app/src/main/res/values-zh-rTW/strings.xml | 4 +- app/src/main/res/values/strings.xml | 4 +- 8 files changed, 703 insertions(+), 252 deletions(-) create mode 100644 app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageModel.java diff --git a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageActivity.java index 808ac85da..d5a6dd7a2 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageActivity.java @@ -60,6 +60,7 @@ public final class HugePageActivity extends AppCompatActivity { private static final String CRASH_FILE = pathJoin(MAGISK_BASE, "crash"); private static final long PAGE_SIZE = 2L * 1024 * 1024; // 2MiB per page private final Handler handler = new Handler(Looper.getMainLooper()); + private final HugePageModel model = new HugePageModel(); private boolean resumed = false; private MaterialToolbar toolbar; private MaterialCardView cardCrashWarning; @@ -171,28 +172,53 @@ private void scheduleRefresh() { } private void refreshStatus() { + refreshStatus(false); + } + + /** + * @param forceCaps re-probe module capabilities immediately (bypass the + * cache) - pass true right after loading/unloading so the UI + * reflects the new module state without the cache's lag. + */ + private void refreshStatus(boolean forceCaps) { runOnPool(() -> { + var caps = model.caps(forceCaps); var moduleInst = shellCheckExists(MODULE_PROP); - var moduleLoaded = shellCheckExists(SYSFS_BASE); + var moduleLoaded = caps.loaded; var moduleDisabled = shellCheckExists(DISABLE_FILE); var crashStamp = shellCheckExists(CRASH_FILE); var refillStat = ""; var servedSummary = ""; + var koAttribution = false; if (moduleLoaded) try { refillStat = shellReadFile(pathJoin(SYSFS_PARAMS, "refill_stat")); // Reconcile + read per-VM served pages so this screen's bar shows // the exact same per-VM breakdown as the usage screen's bar. - if (shellCheckExists(pathJoin(SYSFS_PARAMS, "served_summary"))) { + if (caps.koAttribution) { run("echo 1 > %s/reconcile", SYSFS_PARAMS); servedSummary = shellReadFile(pathJoin(SYSFS_PARAMS, "served_summary")); + koAttribution = true; } } catch (Exception e) { Log.w(TAG, "Failed to read parameters", e); } var stats = parseProp(refillStat); - var owners = parseServedOwners(servedSummary); + // pid -> friendly VM name, so each used segment is labelled "w11" + // like the usage screen. Old modules without the KO attribution API + // (no served_summary) fall back to a system THP scan of running VMs, + // mirroring the usage screen's "scan" mode, instead of showing the + // unavailable per-VM module-attribution breakdown. + List owners; + Map vmMap; + if (koAttribution) { + owners = parseServedOwners(servedSummary); + vmMap = owners.isEmpty() ? new LinkedHashMap<>() : model.vmNames(false); + } else { + vmMap = moduleLoaded ? model.vmNames(false) : new LinkedHashMap<>(); + owners = model.scanThpPages(vmMap.keySet()); + } runOnUiThread(() -> updateUI( - moduleInst, moduleLoaded, moduleDisabled, crashStamp, stats, owners + moduleInst, moduleLoaded, moduleDisabled, crashStamp, stats, owners, vmMap )); }); } @@ -242,7 +268,8 @@ private void updateUI( boolean installed, boolean loaded, boolean disabled, boolean crashed, @NonNull Map stats, - @NonNull List owners + @NonNull List owners, + @NonNull Map vmMap ) { if (isFinishing()) return; cardCrashWarning.setVisibility(crashed ? VISIBLE : GONE); @@ -260,41 +287,50 @@ private void updateUI( // modules without pool_want fall back to capacity. var poolWant = getPages(stats, "pool_want"); if (poolWant <= 0) poolWant = poolTotal; - // Used = pages currently served to VMs (traced). Prefer the live - // served counter; pool_total - avail under-counts after a shrink - // that left served pages out. Old modules: fall back to that. - var used = stats.containsKey("served") - ? getPages(stats, "served") - : Math.clamp(poolTotal - poolAvail, 0, poolTotal); - // 2x2 caption (identical to the usage screen): used / available - // on top, total / pool-size below. Total = real held reserve - // (used + avail = owned + traced), shown raw - no clamp to the - // pool size, so a kernel that fails to release on shrink shows up - // as total > the size you set. - var held = used + poolAvail; - setPagesString(tvPoolUsed, R.string.hugepage_stat_pool_used, used); - setPagesString(tvPoolAvail, R.string.hugepage_stat_pool_available, poolAvail); - setPagesString(tvPoolTotal, R.string.hugepage_stat_pool_total, held); - setPagesString(tvPoolSize, R.string.hugepage_stat_pool_size, poolWant); - // Segmented bar: one colored segment per VM (same per-VM colours - // and deficit block as the usage screen's bar) + a track for the - // available portion; total = want. + // Apple-storage-bar style: one labelled colored block per VM + // (used), then the available portion as a track-coloured gap, + // then the waiting-to-acquire (deficit) block pinned flush right. + // Each block draws its label inside if wide enough. boolean dark = HugePageColor.isDark(this); int n = owners.size(); - int[] colors = new int[n + 1]; - float[] values = new float[n + 1]; + int[] usedColors = new int[n]; + float[] usedValues = new float[n]; + String[] usedLabels = new String[n]; long seg = 0; for (int i = 0; i < n; i++) { int pid = (int) owners.get(i)[0]; long ownerPages = owners.get(i)[1]; - colors[i] = HugePageColor.forPid(pid, dark); - values[i] = ownerPages; + usedColors[i] = HugePageColor.forPid(pid, dark); + usedValues[i] = ownerPages; + String name = vmMap.get(pid); + if (name == null) name = getString(R.string.hugepage_proc_pid, pid); + // Two stacked lines: label over capacity. + usedLabels[i] = name + "\n" + SizeUtils.formatSize(ownerPages * PAGE_SIZE); seg += ownerPages; } - colors[n] = HugePageColor.pending(this); - values[n] = Math.max(0, poolWant - seg - poolAvail); - float total = Math.max(poolWant, seg + poolAvail); - segPoolBar.setData(colors, values, total); + // "used" is the sum of the segments the bar draws, so the caption + // and the bar always agree (one canonical quantity: the per-VM + // served/scanned pages). The kernel 'served' counter can include + // orphaned owner-gone pages that have no segment; those surface in + // the held/available gap rather than as an invisible caption delta. + long used = seg; + long deficit = Math.max(0, poolWant - seg - poolAvail); + // 2x2 caption: used / available on top, total / pool-size below. + // Total = real held reserve (used + avail), shown raw - no clamp + // to the pool size, so a kernel that fails to release on shrink + // shows up as total > the size you set. + var held = used + poolAvail; + setPagesString(tvPoolUsed, R.string.hugepage_stat_pool_used, used); + setPagesString(tvPoolAvail, R.string.hugepage_stat_pool_available, poolAvail); + setPagesString(tvPoolTotal, R.string.hugepage_stat_pool_total, held); + setPagesString(tvPoolSize, R.string.hugepage_stat_pool_size, poolWant); + segPoolBar.setStorage(usedColors, usedValues, usedLabels, + poolAvail, getString(R.string.hugepage_bar_available) + + "\n" + SizeUtils.formatSize(poolAvail * PAGE_SIZE), + HugePageColor.pending(this), deficit, + getString(R.string.hugepage_proc_deficit) + + "\n" + SizeUtils.formatSize(deficit * PAGE_SIZE), + poolWant); } catch (NumberFormatException e) { tvPoolUsed.setText("-"); tvPoolAvail.setText("-"); @@ -354,7 +390,7 @@ private void doLoad() { runOnUiThread(() -> { Toast.makeText(this, R.string.hugepage_already_loaded, LENGTH_SHORT).show(); - refreshStatus(); + refreshStatus(true); }); return; } @@ -378,7 +414,7 @@ private void doLoad() { ? R.string.hugepage_loaded : R.string.hugepage_load_failed, LENGTH_SHORT).show(); - refreshStatus(); + refreshStatus(true); }); }); } @@ -596,7 +632,7 @@ private void doUnload() { R.string.hugepage_unloaded : R.string.hugepage_unload_failed; Toast.makeText(this, msg, LENGTH_SHORT).show(); - refreshStatus(); + refreshStatus(true); }); }); } diff --git a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageModel.java b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageModel.java new file mode 100644 index 000000000..0e1958c42 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageModel.java @@ -0,0 +1,376 @@ +package cn.classfun.droidvm.ui.hugepage; + +import static cn.classfun.droidvm.lib.utils.FileUtils.shellCheckExists; +import static cn.classfun.droidvm.lib.utils.FileUtils.shellReadFile; +import static cn.classfun.droidvm.lib.utils.RunUtils.run; +import static cn.classfun.droidvm.lib.utils.StringUtils.pathJoin; + +import android.os.SystemClock; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.regex.Pattern; + +import cn.classfun.droidvm.lib.daemon.DaemonConnection; + +/** + * Shared huge-page data layer for the status ({@link HugePageActivity}) and + * process ({@link HugePageProcessActivity}) screens. + * + *

Both screens run a per-second refresh loop, so this class caches the three + * expensive sources with short TTLs so a refresh no longer re-probes root sysfs, + * blocks on the daemon, or re-scans {@code /proc} every single tick: + *

    + *
  • capabilities — one batched root probe of the module's sysfs knobs, + * resolved at most once per {@link #CAPS_TTL_MS} (a static property of the + * loaded {@code .ko}, not something to re-derive every tick);
  • + *
  • VM names — the daemon {@code vm_list} pid→name map, fetched at + * most once per {@link #VM_TTL_MS} and reused (last-known) on a slow daemon + * so labels never blank to raw pids;
  • + *
  • pool_target — the read-only v6 grow target, read once and cached.
  • + *
+ * Each screen owns its own instance (separate caches); everything here is + * context-free (pure data), so it never holds an Activity reference. + */ +final class HugePageModel { + static final String SYSFS_BASE = "/sys/module/gh_hugepage_reserve"; + static final String SYSFS_PARAMS = pathJoin(SYSFS_BASE, "parameters"); + static final String SYSFS_OWNERS = pathJoin(SYSFS_PARAMS, "vm_owners"); + /** A THP huge page is 2 MiB, i.e. 2048 KiB. */ + static final long KB_PER_PAGE = 2048; + static final long PAGE_BYTES = KB_PER_PAGE * 1024; + + /** meminfo/smaps values are " kB"; strip everything but the digits once. */ + private static final Pattern NON_DIGITS = Pattern.compile("[^0-9]"); + + private static final long CAPS_TTL_MS = 3000; + private static final long VM_TTL_MS = 4000; + private static final long DAEMON_TIMEOUT_S = 2; + + /* ================================================================== */ + /* Module capabilities */ + /* ================================================================== */ + + /** Immutable snapshot of what the currently-loaded module supports. */ + static final class Caps { + /** {@code /sys/module/gh_hugepage_reserve} exists (module is loaded). */ + final boolean loaded; + /** served_summary present: per-VM KO attribution API (v7). */ + final boolean koAttribution; + /** pool_want knob present (v7 resizable target). */ + final boolean hasPoolWant; + /** acquire knob present (v7 async grow). */ + final boolean hasAcquire; + /** manual_refill knob present (v6 refill). */ + final boolean hasManualRefill; + + Caps(boolean loaded, boolean koAttribution, boolean hasPoolWant, + boolean hasAcquire, boolean hasManualRefill) { + this.loaded = loaded; + this.koAttribution = koAttribution; + this.hasPoolWant = hasPoolWant; + this.hasAcquire = hasAcquire; + this.hasManualRefill = hasManualRefill; + } + } + + @Nullable + private volatile Caps capsCache; + private volatile long capsAtMs = Long.MIN_VALUE; + + /** + * Module capabilities, cached for {@link #CAPS_TTL_MS}. Resolving this from a + * cache (instead of re-probing every refresh) is what stops a single flaky + * root {@code test -e} from flapping the whole view between KO and scan mode. + * Pass {@code force = true} right after an action that changes the module + * (load / unload / soft-disable) to re-probe immediately. + */ + @NonNull + Caps caps(boolean force) { + long now = SystemClock.elapsedRealtime(); + var c = capsCache; + if (!force && c != null && now - capsAtMs < CAPS_TTL_MS) return c; + c = probeCaps(); + capsCache = c; + capsAtMs = now; + return c; + } + + /** + * One batched root round-trip that tests every knob at once (5 {@code test -e} + * in a single {@code run}, instead of one exec per knob). If it comes back + * "not loaded" we retry once, since a lone root {@code test -e} can flake + * under load and we don't want to spuriously report the module gone. + */ + @NonNull + private Caps probeCaps() { + var caps = probeCapsOnce(); + if (!caps.loaded) caps = probeCapsOnce(); // absorb a flaky probe + return caps; + } + + @NonNull + private Caps probeCapsOnce() { + String raw; + try { + raw = run( + "for f in base served_summary pool_want acquire manual_refill; do " + + "if [ \"$f\" = base ]; then t=%1$s; else t=%2$s/$f; fi; " + + "[ -e \"$t\" ] && echo 1 || echo 0; done", + SYSFS_BASE, SYSFS_PARAMS + ).getOutString(); + } catch (Exception e) { + raw = ""; + } + var f = raw.split("\n"); + boolean loaded = flag(f, 0); + boolean served = flag(f, 1); + return new Caps(loaded, loaded && served, flag(f, 2), flag(f, 3), flag(f, 4)); + } + + private static boolean flag(@NonNull String[] lines, int i) { + return i < lines.length && "1".equals(lines[i].trim()); + } + + /* ================================================================== */ + /* VM name resolution (daemon vm_list) */ + /* ================================================================== */ + + @NonNull + private volatile Map vmNames = new LinkedHashMap<>(); + private volatile long vmNamesAtMs = Long.MIN_VALUE; + private volatile boolean vmNamesInit = false; + + /** + * Best-effort pid→VM-name map from the daemon, cached for + * {@link #VM_TTL_MS}. Within the TTL the cached map is returned without + * contacting the daemon, so the refresh loop no longer blocks up to 2s every + * tick. On a fetch that times out the last known map is kept (labels stay + * stable instead of blanking to raw pids). Pass {@code force = true} to + * bypass the cache (e.g. right after killing a process). + */ + @NonNull + Map vmNames(boolean force) { + long now = SystemClock.elapsedRealtime(); + if (!force && vmNamesInit && now - vmNamesAtMs < VM_TTL_MS) return vmNames; + var fresh = fetchVmMap(); + if (fresh != null) { // null == timed out / error: keep last known + vmNames = fresh; + vmNamesAtMs = now; + vmNamesInit = true; + } + return vmNames; + } + + /** + * One blocking daemon {@code vm_list} round-trip (bounded to + * {@link #DAEMON_TIMEOUT_S}). Returns {@code null} on timeout/error so the + * caller can distinguish "no answer" from a genuine empty VM list. + * + *

The map is published through an {@link AtomicReference} set inside the + * callback, so the caller either sees a fully-built map or {@code null} — + * never a half-populated one, even when the response lands right at the + * timeout boundary. + */ + @Nullable + private Map fetchVmMap() { + var result = new AtomicReference>(null); + var latch = new CountDownLatch(1); + DaemonConnection.getInstance().buildRequest("vm_list") + .onResponse(resp -> { + try { + var map = new LinkedHashMap(); + var arr = resp.optJSONArray("data"); + if (arr != null) { + for (int i = 0; i < arr.length(); i++) { + var obj = arr.optJSONObject(i); + if (obj == null) continue; + int pid = obj.optInt("pid", -1); + var name = obj.optString("name", null); + if (pid > 0 && name != null) map.put(pid, name); + } + } + result.set(map); + } finally { + latch.countDown(); + } + }) + .onError(e -> latch.countDown()) + .invoke(); + try { + //noinspection ResultOfMethodCallIgnored + latch.await(DAEMON_TIMEOUT_S, TimeUnit.SECONDS); + } catch (InterruptedException ignored) { + } + return result.get(); + } + + /* ================================================================== */ + /* /proc scanning */ + /* ================================================================== */ + + /** Live /proc state for a pid: run state, THP occupancy, liveness. */ + static final class ProcInfo { + final char state; + /** AnonHugePages + ShmemPmdMapped in KiB, or -1 if unknown. */ + final long thpKb; + final boolean alive; + + ProcInfo(char state, long thpKb, boolean alive) { + this.state = state; + this.thpKb = thpKb; + this.alive = alive; + } + } + + /** + * Batch-read live /proc state (R/S/D + liveness) and THP occupancy + * (AnonHugePages + ShmemPmdMapped) for the given pids in a single root call. + */ + @NonNull + Map readProcInfo(@NonNull Collection pids) { + var map = new LinkedHashMap(); + if (pids.isEmpty()) return map; + var pidList = new StringBuilder(); + for (var pid : pids) { + if (pidList.length() > 0) pidList.append(' '); + pidList.append(pid); + } + String raw; + try { + raw = run( + "for p in %s; do echo \"@@@ $p\"; " + + "grep -E '^State:' /proc/$p/status 2>/dev/null; " + + "grep -E '^AnonHugePages:|^ShmemPmdMapped:' " + + "/proc/$p/smaps_rollup 2>/dev/null; " + + "done", + pidList.toString() + ).getOutString(); + } catch (Exception e) { + return map; + } + int curPid = -1; + char state = '?'; + long thp = -1; + boolean alive = false; + for (var line : raw.split("\n")) { + if (line.startsWith("@@@ ")) { + if (curPid > 0) map.put(curPid, new ProcInfo(state, thp, alive)); + try { + curPid = Integer.parseInt(line.substring(4).trim()); + } catch (NumberFormatException e) { + curPid = -1; + } + state = '?'; + thp = -1; + alive = false; + } else if (line.startsWith("State:")) { + var v = line.substring(6).trim(); + if (!v.isEmpty()) state = v.charAt(0); + alive = true; // status readable -> process exists + } else if (line.startsWith("AnonHugePages:") + || line.startsWith("ShmemPmdMapped:")) { + var digits = NON_DIGITS.matcher(line).replaceAll(""); + if (!digits.isEmpty()) { + try { + thp = (thp < 0 ? 0 : thp) + Long.parseLong(digits); + } catch (NumberFormatException ignored) { + } + } + } + } + if (curPid > 0) map.put(curPid, new ProcInfo(state, thp, alive)); + return map; + } + + /** + * Per-VM THP occupancy in pages for the given pids, as {pid, pages} + * rows, dropping any VM with under one full huge page. Used by the status + * screen's scan-fallback bar; a thin wrapper over {@link #readProcInfo}. + */ + @NonNull + List scanThpPages(@NonNull Collection pids) { + var result = new ArrayList(); + var procInfo = readProcInfo(pids); + for (var pid : pids) { + var info = procInfo.get(pid); + if (info == null || info.thpKb <= 0) continue; + long pages = info.thpKb / KB_PER_PAGE; + if (pages > 0) result.add(new long[]{pid, pages}); + } + return result; + } + + /* ================================================================== */ + /* Pool stats */ + /* ================================================================== */ + + private volatile long poolTargetCache = -1; + private volatile long poolTargetAtMs = Long.MIN_VALUE; + + /** + * v6 grow target from the read-only {@code pool_target} param, cached with a + * short TTL (it is static until the module is reloaded / the app restarts). + * Returns -1 if unavailable. This is what stops the v6 refresh loop from + * re-{@code cat}-ing the same static file every tick. + */ + private long poolTarget() { + long now = SystemClock.elapsedRealtime(); + if (poolTargetAtMs != Long.MIN_VALUE && now - poolTargetAtMs < CAPS_TTL_MS) { + return poolTargetCache; + } + long v = -1; + try { + var t = shellReadFile(pathJoin(SYSFS_PARAMS, "pool_target")).trim(); + if (!t.isEmpty()) v = Long.parseLong(t); + } catch (Exception ignored) { + } + poolTargetCache = v; + poolTargetAtMs = now; + return v; + } + + /** + * Read the pool counters from {@code refill_stat}. + * Returns {@code {total, avail, want, acquiring, served}} in pages, or + * {@code null} if the module isn't loaded / stats are unreadable. {@code want} + * falls back to v6's cached {@code pool_target}, then to capacity. + */ + @Nullable + long[] readPoolPages() { + try { + if (!shellCheckExists(SYSFS_BASE)) return null; + var raw = shellReadFile(pathJoin(SYSFS_PARAMS, "refill_stat")); + long total = -1, avail = 0, want = -1, acquiring = 0, served = 0; + for (var line : raw.split("\n")) { + line = line.trim(); + if (line.startsWith("pool_total=")) { + total = Long.parseLong(line.substring(11).trim()); + } else if (line.startsWith("pool_avail=")) { + avail = Long.parseLong(line.substring(11).trim()); + } else if (line.startsWith("pool_want=")) { + want = Long.parseLong(line.substring(10).trim()); + } else if (line.startsWith("acquire_active=")) { + acquiring = Long.parseLong(line.substring(15).trim()); + } else if (line.startsWith("served=")) { + served = Long.parseLong(line.substring(7).trim()); + } + } + if (total < 0) return null; + if (want < 0) want = poolTarget(); // v6: separate read-only knob + if (want < 0) want = total; + return new long[]{total, avail, want, acquiring, served}; + } catch (Exception e) { + return null; + } + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcessActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcessActivity.java index b23b37752..1f1bc9e00 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcessActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcessActivity.java @@ -4,7 +4,6 @@ import static android.view.View.VISIBLE; import static android.widget.Toast.LENGTH_LONG; import static android.widget.Toast.LENGTH_SHORT; -import static cn.classfun.droidvm.lib.utils.FileUtils.shellCheckExists; import static cn.classfun.droidvm.lib.utils.FileUtils.shellReadFile; import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; import static cn.classfun.droidvm.lib.utils.ProcessUtils.SIGKILL; @@ -35,15 +34,11 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder; import java.util.ArrayList; -import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; import cn.classfun.droidvm.R; -import cn.classfun.droidvm.lib.daemon.DaemonConnection; import cn.classfun.droidvm.lib.size.SizeUtils; /** @@ -65,14 +60,19 @@ public final class HugePageProcessActivity extends AppCompatActivity private static final long HUGE_PAGE_BYTES = 2L * 1024 * 1024; private final Handler handler = new Handler(Looper.getMainLooper()); + private final HugePageModel model = new HugePageModel(); private boolean resumed = false; + // scanMode = the user's preference; koPresent = whether the KO attribution + // API is available. The effective mode shown is (scanMode || !koPresent) - + // forced to scan whenever KO is absent - computed on demand so a menu tap is + // reflected immediately instead of after the next background refresh. private boolean scanMode = false; + private volatile boolean koPresent = true; + // Edge-trigger the "KO unavailable" toast on the present->absent transition + // only, so a stably-unavailable module doesn't re-toast every refresh. + private boolean koWasPresent = true; private boolean firstRefresh = true; private volatile boolean acquireWatching = false; - // The KO-vs-THP fallback is resolved at most once (on entry). After that, - // and after any manual menu choice, we never auto-switch again - otherwise a - // single flaky root `test -e` under load would permanently bounce KO->THP. - private volatile boolean modeResolved = false; private MaterialToolbar toolbar; private RecyclerView recyclerView; private TextView tvEmpty; @@ -139,38 +139,28 @@ private void updateModeChecked() { var menu = toolbar.getMenu(); var ko = menu.findItem(R.id.menu_mode_ko); var scan = menu.findItem(R.id.menu_mode_scan); - if (ko != null) ko.setIcon(modeIcon(!scanMode)); - if (scan != null) scan.setIcon(modeIcon(scanMode)); + // Effective mode is derived from the user's choice + KO availability, so a + // just-tapped mode is reflected instantly (not after the next refresh); + // the KO entry is disabled outright when its attribution API is absent. + boolean effectiveScan = scanMode || !koPresent; + if (ko != null) { + ko.setIcon(modeIcon(!effectiveScan)); + ko.setEnabled(koPresent); + } + if (scan != null) scan.setIcon(modeIcon(effectiveScan)); } private static int modeIcon(boolean active) { return active ? R.drawable.ic_circle_filled : R.drawable.ic_circle_outline; } - /** User picked a mode from the menu (session-only; entry always defaults KO). */ + /** User picked a mode from the menu (session-only preference). */ private void setMode(boolean scan) { scanMode = scan; - modeResolved = true; // explicit choice: never auto-switch again updateModeChecked(); refresh(); } - /** - * KO was selected but its attribution API is absent (module not loaded, or - * an old .ko). Switch the effective mode to scan for this session without - * overwriting any saved preference, and tell the user why. A present-but- - * empty pool (no VM) never reaches here - that stays in KO. - */ - private void fallbackToScan() { - if (!scanMode) { - scanMode = true; - updateModeChecked(); - Toast.makeText(this, R.string.hugepage_proc_ko_unavailable, - LENGTH_SHORT).show(); - } - } - - @Override protected void onResume() { super.onResume(); @@ -205,22 +195,13 @@ private void refresh() { } runOnPool(() -> { boolean dark = HugePageColor.isDark(this); - boolean useScan = scanMode; - boolean autoSwitched = false; - // ONE-SHOT: only on the first resolve do we consider falling back to - // scan when the KO attribution API is absent (module not loaded, or - // an old .ko without served_summary). After that we never re-probe, - // so a transient root `test -e` failure can't flip a working KO view - // to THP. A present-but-empty pool (no VM) stays in KO ("no VM"). - if (!modeResolved) { - if (!useScan && !koApiPresent() && !koApiPresent()) { - // Probed twice (a single root `test -e` can flake under load); - // only fall back to scan if the KO API is really absent. - useScan = true; - autoSwitched = true; - } - modeResolved = true; - } + // KO attribution availability comes from the cached capability probe + // (re-probed at most every few seconds, with a retry on a negative), + // so a single flaky root `test -e` can't flap the whole view between + // KO and scan every tick. Absent -> force scan, disable the KO entry. + // A present-but-empty pool (no VM) keeps KO available ("no VM"). + boolean koNow = model.caps(false).koAttribution; + boolean useScan = scanMode || !koNow; List list = new ArrayList<>(); String emptyText; @@ -275,7 +256,7 @@ private void refresh() { long tracedKb = 0; for (var r : list) if (r.thpKb > 0) tracedKb += r.thpKb; long holdKb, wantKb; - var pool = readPoolPages(); + var pool = model.readPoolPages(); boolean kernelAcquiring = pool != null && pool.length >= 4 && pool[3] != 0; if (pool != null) { holdKb = pool[1] * 2048; // avail @@ -293,13 +274,20 @@ private void refresh() { var finalList = list; var finalEmpty = emptyText; - var finalAuto = autoSwitched; + var fKoNow = koNow; var fTraced = tracedKb; var fHold = holdKb; var fWant = wantKb; var fAcquiring = kernelAcquiring; runOnUiThread(() -> { - if (finalAuto) fallbackToScan(); + koPresent = fKoNow; + // Toast only on the present->absent edge while the user wants KO, + // so a stably-unavailable module doesn't re-toast every refresh. + if (koWasPresent && !fKoNow && !scanMode) { + Toast.makeText(this, R.string.hugepage_proc_ko_unavailable, + LENGTH_SHORT).show(); + } + koWasPresent = fKoNow; // Re-sync the menu radio every refresh. Doing it once in onCreate // (right after inflateMenu, before the menu is laid out) doesn't // stick, leaving the radio showing the wrong mode. @@ -310,65 +298,6 @@ private void refresh() { }); } - /** True if the loaded module exposes the KO attribution API (v7+). */ - private boolean koApiPresent() { - return shellCheckExists(SYSFS_BASE) - && shellCheckExists(pathJoin(SYSFS_PARAMS, "served_summary")); - } - - @Nullable - private long[] readPoolPages() { - try { - if (!shellCheckExists(SYSFS_BASE)) return null; - var raw = shellReadFile(pathJoin(SYSFS_PARAMS, "refill_stat")); - long total = -1, avail = 0, want = -1, acquiring = 0, served = 0; - for (var line : raw.split("\n")) { - line = line.trim(); - if (line.startsWith("pool_total=")) { - total = Long.parseLong(line.substring(11).trim()); - } else if (line.startsWith("pool_avail=")) { - avail = Long.parseLong(line.substring(11).trim()); - } else if (line.startsWith("pool_want=")) { - want = Long.parseLong(line.substring(10).trim()); - } else if (line.startsWith("acquire_active=")) { - acquiring = Long.parseLong(line.substring(15).trim()); - } else if (line.startsWith("served=")) { - served = Long.parseLong(line.substring(7).trim()); - } - } - if (total < 0) return null; - // Old modules have no pool_want -> treat want == capacity. - if (want < 0) want = total; - return new long[]{total, avail, want, acquiring, served}; - } catch (Exception e) { - return null; - } - } - - /** - * Batch-read live /proc state (R/S/D + liveness) and THP occupancy - * (AnonHugePages + ShmemPmdMapped) for the given pids in a single root call. - * Shared by KO attribution (buildFromOwners) and THP scan (scanProc). - */ - @NonNull - private Map readProcInfo(@NonNull Collection pids) { - if (pids.isEmpty()) return new HashMap<>(); - var pidList = new StringBuilder(); - for (var pid : pids) { - if (pidList.length() > 0) pidList.append(' '); - pidList.append(pid); - } - var procRaw = run( - "for p in %s; do echo \"@@@ $p\"; " + - "grep -E '^State:' /proc/$p/status 2>/dev/null; " + - "grep -E '^AnonHugePages:|^ShmemPmdMapped:' " + - "/proc/$p/smaps_rollup 2>/dev/null; " + - "done", - pidList.toString() - ).getOutString(); - return parseProcInfo(procRaw); - } - @NonNull private List buildFromOwners( @NonNull List owners, boolean dark, @@ -376,10 +305,10 @@ private List buildFromOwners( ) { var pids = new ArrayList(); for (var o : owners) pids.add(o.pid); - var procInfo = readProcInfo(pids); + var procInfo = model.readProcInfo(pids); // Best-effort map pid -> VM name (only running VMs have a live pid). - var vmMap = fetchVmMap(); + var vmMap = model.vmNames(false); var result = new ArrayList(); for (var o : owners) { @@ -409,10 +338,10 @@ private List buildFromOwners( @NonNull private List scanProc(boolean dark) { // Running VMs (pid -> name) come from the daemon. - var vmMap = fetchVmMap(); + var vmMap = model.vmNames(false); if (vmMap.isEmpty()) return new ArrayList<>(); - var procInfo = readProcInfo(vmMap.keySet()); + var procInfo = model.readProcInfo(vmMap.keySet()); var result = new ArrayList(); for (var e : vmMap.entrySet()) { @@ -441,18 +370,6 @@ private static final class Owner { } } - private static final class ProcInfo { - final char state; - final long thpKb; - final boolean alive; - - ProcInfo(char state, long thpKb, boolean alive) { - this.state = state; - this.thpKb = thpKb; - this.alive = alive; - } - } - @NonNull private List parseOwners(@NonNull String raw) { var list = new ArrayList(); @@ -483,75 +400,6 @@ private List parseOwners(@NonNull String raw) { return list; } - @NonNull - private Map parseProcInfo(@NonNull String raw) { - var map = new HashMap(); - int curPid = -1; - char state = '?'; - long thp = -1; // -1 = unknown; becomes sum of Anon + Shmem THP - boolean alive = false; - for (var line : raw.split("\n")) { - if (line.startsWith("@@@ ")) { - if (curPid > 0) - map.put(curPid, new ProcInfo(state, thp, alive)); - try { - curPid = Integer.parseInt(line.substring(4).trim()); - } catch (NumberFormatException e) { - curPid = -1; - } - state = '?'; - thp = -1; - alive = false; - } else if (line.startsWith("State:")) { - var v = line.substring(6).trim(); - if (!v.isEmpty()) state = v.charAt(0); - alive = true; // status readable -> process exists - } else if (line.startsWith("AnonHugePages:") - || line.startsWith("ShmemPmdMapped:")) { - var digits = line.replaceAll("[^0-9]", ""); - if (!digits.isEmpty()) { - try { - thp = (thp < 0 ? 0 : thp) + Long.parseLong(digits); - } catch (NumberFormatException ignored) { - } - } - } - } - if (curPid > 0) map.put(curPid, new ProcInfo(state, thp, alive)); - return map; - } - - @NonNull - private Map fetchVmMap() { - var map = new HashMap(); - var latch = new CountDownLatch(1); - DaemonConnection.getInstance().buildRequest("vm_list") - .onResponse(resp -> { - try { - var arr = resp.optJSONArray("data"); - if (arr != null) { - for (int i = 0; i < arr.length(); i++) { - var obj = arr.optJSONObject(i); - if (obj == null) continue; - int pid = obj.optInt("pid", -1); - var name = obj.optString("name", null); - if (pid > 0 && name != null) map.put(pid, name); - } - } - } finally { - latch.countDown(); - } - }) - .onError(e -> latch.countDown()) - .invoke(); - try { - //noinspection ResultOfMethodCallIgnored - latch.await(2, TimeUnit.SECONDS); - } catch (InterruptedException ignored) { - } - return map; - } - private void showResult( @NonNull List list, long usedKb, long availKb, long wantKb, @NonNull String emptyText @@ -560,19 +408,28 @@ private void showResult( firstRefresh = false; adapter.submit(list); - // Segmented bar: one colored segment per row, including the - // "waiting for acquire" (deficit) row. Track = available; total = want. - int n = list.size(); - int[] colors = new int[n]; - float[] values = new float[n]; - long seg = 0; - for (int i = 0; i < n; i++) { - colors[i] = list.get(i).color; - values[i] = Math.max(0, list.get(i).thpKb); - seg += values[i]; + // Segmented bar (shared builder): VM segments (used), then the available + // portion as a track-coloured gap, then the "waiting for acquire" deficit + // pinned flush right. This screen's bar is a plain unlabelled meter. + int used = 0; + for (var p : list) if (!p.acquire) used++; + int[] usedColors = new int[used]; + float[] usedValues = new float[used]; + int deficitColor = HugePageColor.pending(this); + long deficit = 0; + int u = 0; + for (var p : list) { + if (p.acquire) { // deficit rows: colour + value + deficitColor = p.color; + deficit += Math.max(0, p.thpKb); + } else { // used: one segment per VM + usedColors[u] = p.color; + usedValues[u] = Math.max(0, p.thpKb); + u++; + } } - float total = Math.max(wantKb, seg + Math.max(0, availKb)); - segBar.setData(colors, values, total); + segBar.setStorage(usedColors, usedValues, null, + availKb, null, deficitColor, deficit, null, wantKb); // 2x2 caption: used / available on top, total / pool-size below. // Total = the real held reserve (used + avail = owned + traced), shown @@ -644,11 +501,13 @@ public void onAcquire() { // blocked. The kernel 'acquire' write returns immediately; the worker // migrates in the background and we watch acquire_active for completion. new Thread(() -> { + var caps = model.caps(false); + boolean v7 = caps.hasAcquire; boolean triggered = false; try { - if (shellCheckExists(pathJoin(SYSFS_PARAMS, "acquire"))) { + if (v7) { triggered = run("echo 1 > %s/acquire", SYSFS_PARAMS).isSuccess(); - } else if (shellCheckExists(pathJoin(SYSFS_PARAMS, "manual_refill"))) { + } else if (caps.hasManualRefill) { triggered = run("echo 1 > %s/manual_refill", SYSFS_PARAMS).isSuccess(); } } catch (Exception e) { @@ -660,12 +519,24 @@ public void onAcquire() { R.string.hugepage_refill_failed, LENGTH_SHORT).show()); return; } - // Poll until the worker clears acquire_active. Each tick nudges the - // UI to re-read refill_stat so the count visibly climbs. + if (!v7) { + // v6 (manual_refill): an async refill with no acquire_active to + // poll and no served=/pool_want to report a precise got/want. + // Just trigger it and let the periodic refresh animate the bar. + acquireWatching = false; + runOnUiThread(() -> { + Toast.makeText(this, R.string.hugepage_proc_acquire_done, + LENGTH_SHORT).show(); + refresh(); + }); + return; + } + // v7: poll until the worker clears acquire_active. Each tick nudges + // the UI to re-read refill_stat so the count visibly climbs. long[] pages = null; for (int i = 0; i < ACQUIRE_POLL_MAX; i++) { try { Thread.sleep(ACQUIRE_POLL_MS); } catch (InterruptedException ignored) {} - pages = readPoolPages(); // {total, avail, want, acquiring, served} + pages = model.readPoolPages(); // {total, avail, want, acquiring, served} runOnUiThread(this::refresh); // animate climbing numbers if (pages == null || pages.length < 5 || pages[3] == 0) break; } diff --git a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/SegmentedBar.java b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/SegmentedBar.java index 51d866ba3..a5c864ffd 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/SegmentedBar.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/SegmentedBar.java @@ -2,10 +2,12 @@ import android.content.Context; import android.graphics.Canvas; +import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.graphics.RectF; import android.util.AttributeSet; +import android.util.TypedValue; import android.view.View; import androidx.annotation.NonNull; @@ -19,16 +21,37 @@ * track for the remaining/available portion. Widths are proportional; the caller * passes a {@code total} that is always >= the sum of segment values, so the * segments can never overflow the bar (no crash on over-accounting). + * + *

An optional {@code labels} array enables an Apple-storage-bar style: each + * segment wide enough to fit its label draws it inside the colored block, with + * the text colour chosen automatically for contrast. When {@code labels} is null + * the bar stays a plain thin meter. */ public final class SegmentedBar extends View { private int[] colors = new int[0]; private float[] values = new float[0]; + @Nullable + private String[] labels = null; + // Per-segment label data pre-split, pre-measured and pre-coloured in setData, + // so onDraw (a hot path) allocates and measures nothing. labelTop[i] == null + // means "no label for this segment". + @Nullable + private String[] labelTop; + @Nullable + private String[] labelBottom; + private float[] labelTopW = new float[0]; + private float[] labelBotW = new float[0]; + private int[] labelTextColor = new int[0]; private float total = 0f; - private int trackColor; + private final int trackColor; + private final boolean darkTheme; private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint textPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint subTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); private final RectF rect = new RectF(); private final Path clip = new Path(); - private final float radius; + private final float density; + private final float labelPad; public SegmentedBar(Context context) { this(context, null); @@ -36,17 +59,104 @@ public SegmentedBar(Context context) { public SegmentedBar(Context context, @Nullable AttributeSet attrs) { super(context, attrs); - radius = 3f * getResources().getDisplayMetrics().density; + density = getResources().getDisplayMetrics().density; + labelPad = 6f * density; trackColor = ContextCompat.getColor(context, R.color.hugepage_bar_track); + darkTheme = HugePageColor.isDark(context); + var dm = getResources().getDisplayMetrics(); + textPaint.setTextSize(TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_SP, 11f, dm)); // label line + subTextPaint.setTextSize(TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_SP, 10f, dm)); // capacity line } + /** Plain thin meter: segments only, no in-bar labels. */ public void setData(@NonNull int[] colors, @NonNull float[] values, float total) { + setData(colors, values, null, total); + } + + /** Apple-style: each wide-enough segment draws {@code labels[i]} inside it. */ + public void setData(@NonNull int[] colors, @NonNull float[] values, + @Nullable String[] labels, float total) { this.colors = colors; this.values = values; + this.labels = labels; this.total = total; + prepareLabels(); invalidate(); } + /** Pre-split, pre-measure and pre-colour labels so onDraw does none of it. */ + private void prepareLabels() { + if (labels == null) { + labelTop = null; + labelBottom = null; + return; + } + int m = labels.length; + labelTop = new String[m]; + labelBottom = new String[m]; + labelTopW = new float[m]; + labelBotW = new float[m]; + labelTextColor = new int[m]; + for (int i = 0; i < m; i++) { + var label = labels[i]; + if (label == null || label.isEmpty()) { + labelTop[i] = null; // no label for this segment + continue; + } + int nl = label.indexOf('\n'); // "top\nbottom"; no split() regex + labelTop[i] = nl < 0 ? label : label.substring(0, nl); + labelBottom[i] = nl < 0 ? null : label.substring(nl + 1); + labelTopW[i] = textPaint.measureText(labelTop[i]); + labelBotW[i] = labelBottom[i] != null + ? subTextPaint.measureText(labelBottom[i]) : 0f; + labelTextColor[i] = i < colors.length + ? textColorFor(colors[i]) : 0xFFFFFFFF; + } + } + + /** + * Assemble and set a storage-style bar shared by both hugepage screens: + * the {@code used} segments, then the available portion as a track-coloured + * gap, then the deficit ("waiting for acquire") pinned flush against the + * right edge. {@code total = max(want, sum(used) + avail)}, so the deficit + * ends exactly at the bar's right edge. Pass {@code usedLabels != null} for + * the labelled (status-screen) bar or {@code null} for the plain meter. + * + * @param usedColors one colour per used segment + * @param usedValues one value per used segment (same length as usedColors) + * @param usedLabels per-segment labels, or {@code null} for no labels + */ + public void setStorage( + @NonNull int[] usedColors, @NonNull float[] usedValues, + @Nullable String[] usedLabels, + float avail, @Nullable String availLabel, + int deficitColor, float deficit, @Nullable String deficitLabel, + float want + ) { + int n = usedValues.length; + boolean withLabels = usedLabels != null; + int[] c = new int[n + 2]; + float[] v = new float[n + 2]; + String[] l = withLabels ? new String[n + 2] : null; + float seg = 0; + for (int i = 0; i < n; i++) { + c[i] = usedColors[i]; + v[i] = Math.max(0, usedValues[i]); + if (withLabels) l[i] = usedLabels[i]; + seg += v[i]; + } + float a = Math.max(0, avail); + c[n] = trackColor; // available gap + v[n] = a; + if (withLabels) l[n] = availLabel; + c[n + 1] = deficitColor; // deficit, flush right + v[n + 1] = Math.max(0, deficit); + if (withLabels) l[n + 1] = deficitLabel; + setData(c, v, l, Math.max(want, seg + a)); + } + @Override protected void onDraw(@NonNull Canvas canvas) { super.onDraw(canvas); @@ -54,6 +164,7 @@ protected void onDraw(@NonNull Canvas canvas) { float h = getHeight(); if (w <= 0 || h <= 0) return; + float radius = Math.min(6f * density, h / 3f); rect.set(0, 0, w, h); clip.reset(); clip.addRoundRect(rect, radius, radius, Path.Direction.CW); @@ -69,9 +180,60 @@ protected void onDraw(@NonNull Canvas canvas) { for (int i = 0; i < n; i++) { float segW = w * (values[i] / total); if (segW <= 0f) continue; + float segRight = Math.min(x + segW, w); paint.setColor(colors[i]); - canvas.drawRect(x, 0, Math.min(x + segW, w), h, paint); + canvas.drawRect(x, 0, segRight, h, paint); + drawLabel(canvas, i, x, segRight, h); x += segW; } } + + /** + * Draw the block label as two stacked, centred lines: {@code "top\nbottom"} + * (label over capacity). A label with no newline draws as a single line. + * The whole label is omitted if the widest line would not fit the block. + */ + private void drawLabel(@NonNull Canvas canvas, int i, float left, float right, float h) { + if (labelTop == null || i >= labelTop.length) return; + String top = labelTop[i]; + if (top == null) return; // no / empty label + String bottom = labelBottom[i]; + + float segW = right - left; + float topW = labelTopW[i]; + float botW = labelBotW[i]; + if (segW < Math.max(topW, botW) + 2f * labelPad) return; // omit if it won't fit + + int color = labelTextColor[i]; + textPaint.setColor(color); + float topH = textPaint.descent() - textPaint.ascent(); + if (bottom == null) { + float ty = h / 2f - (textPaint.descent() + textPaint.ascent()) / 2f; + canvas.drawText(top, left + (segW - topW) / 2f, ty, textPaint); + return; + } + subTextPaint.setColor(color); + float gap = 1f * density; + float botH = subTextPaint.descent() - subTextPaint.ascent(); + float startY = (h - (topH + gap + botH)) / 2f; + float topBaseline = startY - textPaint.ascent(); + float botBaseline = startY + topH + gap - subTextPaint.ascent(); + canvas.drawText(top, left + (segW - topW) / 2f, topBaseline, textPaint); + canvas.drawText(bottom, left + (segW - botW) / 2f, botBaseline, subTextPaint); + } + + /** + * Pick black or white text for readability over a (possibly translucent) + * segment colour by compositing it over the theme surface and comparing + * perceived luminance. + */ + private int textColorFor(int segColor) { + int a = Color.alpha(segColor); + int bg = darkTheme ? 0xFF1C1B1F : 0xFFFFFFFF; + int r = (Color.red(segColor) * a + Color.red(bg) * (255 - a)) / 255; + int g = (Color.green(segColor) * a + Color.green(bg) * (255 - a)) / 255; + int b = (Color.blue(segColor) * a + Color.blue(bg) * (255 - a)) / 255; + double lum = (0.299 * r + 0.587 * g + 0.114 * b) / 255.0; + return lum > 0.6 ? 0xDE000000 : 0xFFFFFFFF; + } } diff --git a/app/src/main/res/layout/activity_hugepage.xml b/app/src/main/res/layout/activity_hugepage.xml index 8c3c9f8a4..7f2510171 100644 --- a/app/src/main/res/layout/activity_hugepage.xml +++ b/app/src/main/res/layout/activity_hugepage.xml @@ -193,7 +193,7 @@ 模块已加载 池大小不可小于运行中 VM 的内存总和(%1$s) 获取中,无法缩小,请等待获取完成 - 池大小已保存并应用(增大需点获取) + 池大小已保存并应用 获取 获取完成 停止获取 @@ -971,6 +971,8 @@ 模块不足,尝试 userspace 协助… 占用:%1$d 页 (%2$s) 等待获取 + pid %1$d + 可用 目前没有线程占用保留的 hugepage 池。 终止 发送信号失败 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 04983f8ad..ca90d6224 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -929,7 +929,7 @@ 觸發獲取失敗 池大小 池大小已儲存(重啟後生效) - 池大小已儲存並套用(放大需按獲取) + 池大小已儲存並套用 獲取中,無法縮小,請待獲取完成 池大小不可小於執行中 VM 的記憶體總和(%1$s) 儲存池大小失敗 @@ -955,6 +955,8 @@ 未歸屬 未歸屬 — 已釋放 %1$d / 已挪用 %2$d 頁 等待獲取 + pid %1$d + 可用 虛擬機器:%1$s 非追蹤中的虛擬機器 狀態:%1$c (%2$s) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e43ba330b..7692a340f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -970,7 +970,7 @@ Module short; trying userspace assist… Pool Size Pool size saved (effective after reboot) - Pool size saved & applied (grow needs Acquire) + Pool size saved & applied Acquiring — can\'t shrink now, wait for it to finish Pool can\'t be smaller than the running VMs\' total memory (%1$s) Failed to save pool size @@ -996,6 +996,8 @@ Unattributed Unattributed — freed %1$d / reused %2$d pages Waiting for acquire + pid %1$d + Available VM: %1$s Not a tracked VM State: %1$c (%2$s) From 9128ebe6107edeaa9bda9c1f1e3795641cbd6e26 Mon Sep 17 00:00:00 2001 From: HuJK Date: Sun, 5 Jul 2026 10:22:43 +0800 Subject: [PATCH 080/182] hugepage: acquire-mode buttons (v1/v2/v3) with per-mode spinners Expose the module's three acquire algorithms in the UI and reflect which one is running. - Buttons: v1/v2/v3 on the process-list "waiting for acquire" row and a new "Acquire huge pages" item on the main status screen (shared acquire_buttons.xml, same row style as the stat rows). Each writes acquire=1/2/3 via HugePageOps.acquire(int mode, ...). - Per-mode spinner: read acquire_mode from refill_stat (readPoolPages index 5, -1 when the module can't report it). While a run is in flight only the running mode spins (tap = stop) and the other two grey out; an unknown mode (old module) falls back to spinning all three. - Enable only on a real deficit: loaded && pool_want > 0 && avail+served < want. Disabled when not loaded, already at target, or soft-disabled (want=0). - Gentler defaults: pool-size minimum lowered to 0 (ti_min=0MiB); soft-disable writes pool_want=0; Enable/Save auto-fill uses v1 (migrate only, no zram eviction, no reboot risk). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01TaHcvnHaZk8qqpKiS25Hbk --- .../droidvm/ui/hugepage/HugePageActivity.java | 546 ++++++------ .../droidvm/ui/hugepage/HugePageModel.java | 807 ++++++++++++++---- .../ui/hugepage/HugePageProcessActivity.java | 265 ++---- .../ui/hugepage/HugePageProcessAdapter.java | 86 +- .../cn/classfun/droidvm/ui/hugepage/Try.java | 48 ++ app/src/main/res/layout/acquire_buttons.xml | 197 +++++ app/src/main/res/layout/activity_hugepage.xml | 33 +- .../main/res/layout/item_hugepage_process.xml | 32 +- app/src/main/res/values-zh-rCN/strings.xml | 12 + app/src/main/res/values-zh-rTW/strings.xml | 12 + app/src/main/res/values/strings.xml | 12 + 11 files changed, 1367 insertions(+), 683 deletions(-) create mode 100644 app/src/main/java/cn/classfun/droidvm/ui/hugepage/Try.java create mode 100644 app/src/main/res/layout/acquire_buttons.xml diff --git a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageActivity.java index d5a6dd7a2..a4d4be4cb 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageActivity.java @@ -5,11 +5,11 @@ import static android.widget.Toast.LENGTH_SHORT; import static cn.classfun.droidvm.lib.utils.FileUtils.shellCheckExists; import static cn.classfun.droidvm.lib.utils.FileUtils.shellReadFile; -import static cn.classfun.droidvm.lib.utils.RunUtils.run; import static cn.classfun.droidvm.lib.utils.RunUtils.runList; import static cn.classfun.droidvm.lib.utils.StringUtils.pathJoin; import static cn.classfun.droidvm.lib.utils.ThreadUtils.runOnPool; +import android.content.Context; import android.content.Intent; import android.content.res.ColorStateList; import android.graphics.Color; @@ -51,10 +51,7 @@ public final class HugePageActivity extends AppCompatActivity { private static final String TAG = "HugePageActivity"; - private static final String SYSFS_BASE = "/sys/module/gh_hugepage_reserve"; - private static final String SYSFS_PARAMS = pathJoin(SYSFS_BASE, "parameters"); private static final String MAGISK_BASE = "/data/adb/modules/gh-hugepage-reserve"; - private static final String MODULE_PROP = pathJoin(MAGISK_BASE, "module.prop"); private static final String SETTINGS_PROP = pathJoin(MAGISK_BASE, "settings.prop"); private static final String DISABLE_FILE = pathJoin(MAGISK_BASE, "disable"); private static final String CRASH_FILE = pathJoin(MAGISK_BASE, "crash"); @@ -75,7 +72,22 @@ public final class HugePageActivity extends AppCompatActivity { private boolean moduleHasPoolWant = false; private boolean moduleSoftDisabled = false; private boolean moduleAcquiring = false; + // Drives the acquire-mode slots (v1/v2/v3): true while a run is in flight. + // Set optimistically on tap, then reconciled from acquire_active + // (readPoolPages()[3]) by the periodic status refresh. + private boolean mainAcquiring = false; + private int mainAcquireMode = -1; // running v (1/2/3); -1 = unknown (old module) + private boolean wasAcquiring = false; // last-seen acquire_active, for the "done" toast + // Acquire buttons usable only when the module is loaded and there is a deficit + // to fill (avail+served < want); disabled at target, unloaded, or soft-disabled. + private boolean acquireEnabled = false; private MaterialButton btnViewProcesses; + private View btnAcquireV1; + private View btnAcquireV2; + private View btnAcquireV3; + private View progressAcquireV1; + private View progressAcquireV2; + private View progressAcquireV3; private SegmentedBar segPoolBar; private TextView tvPoolUsed; private TextView tvPoolAvail; @@ -105,6 +117,12 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { saveTextColors = btnSavePoolSize.getTextColors(); btnModuleToggle = findViewById(R.id.btn_module_toggle); btnViewProcesses = findViewById(R.id.btn_view_processes); + btnAcquireV1 = findViewById(R.id.btn_acquire_v1); + btnAcquireV2 = findViewById(R.id.btn_acquire_v2); + btnAcquireV3 = findViewById(R.id.btn_acquire_v3); + progressAcquireV1 = findViewById(R.id.progress_acquire_v1); + progressAcquireV2 = findViewById(R.id.progress_acquire_v2); + progressAcquireV3 = findViewById(R.id.progress_acquire_v3); segPoolBar = findViewById(R.id.seg_pool_bar); tvPoolUsed = findViewById(R.id.tv_pool_used); tvPoolAvail = findViewById(R.id.tv_pool_avail); @@ -135,17 +153,122 @@ private void initialize() { // loaded (v6, no knob) -> Disable (rmmod) btnModuleToggle.setOnClickListener(v -> { if (!moduleInstalled) openModulePage(); - else if (!moduleLoaded) doLoad(); - else if (moduleSoftDisabled) doSoftEnable(); - else if (moduleHasPoolWant) doSoftDisable(); - else confirmUnload(); + else if (!moduleLoaded || moduleSoftDisabled) doEnable(); + else if (moduleHasPoolWant) doDisable(); // v7: soft-disable (pool_want=0) + else confirmUnload(); // v6: no soft knob -> confirm rmmod }); btnViewProcesses.setOnClickListener(v -> startActivity( new Intent(this, HugePageProcessActivity.class))); + // Acquire-mode slots: each button starts its mode; each spinner (shown + // while a run is in flight) interrupts it. Listeners are static — the + // slots aren't recycled — and the idle/running visibility toggle is + // driven by applyAcquireState(). + // Short-press runs it (gated to the pressable state); long-press opens the + // what-does-this-do dialog with a Run/Cancel choice (always available, even + // when the button is greyed - the buttons stay enabled for that). + btnAcquireV1.setOnClickListener(v -> { if (acquireEnabled && !mainAcquiring) startAcquire(1); }); + btnAcquireV2.setOnClickListener(v -> { if (acquireEnabled && !mainAcquiring) startAcquire(2); }); + btnAcquireV3.setOnClickListener(v -> { if (acquireEnabled && !mainAcquiring) startAcquire(3); }); + btnAcquireV1.setOnLongClickListener(v -> { showAcquireInfo(this, 1, () -> startAcquire(1)); return true; }); + btnAcquireV2.setOnLongClickListener(v -> { showAcquireInfo(this, 2, () -> startAcquire(2)); return true; }); + btnAcquireV3.setOnLongClickListener(v -> { showAcquireInfo(this, 3, () -> startAcquire(3)); return true; }); + View.OnClickListener stopAcquire = v -> stopMainAcquire(); + progressAcquireV1.setOnClickListener(stopAcquire); + progressAcquireV2.setOnClickListener(stopAcquire); + progressAcquireV3.setOnClickListener(stopAcquire); + applyAcquireState(); cardCrashWarning.setOnClickListener(v -> doDismissCrash()); loadPoolSize(); } + /** + * Reflect {@link #mainAcquiring}/{@link #mainAcquireMode} on the three slots: + * idle -> all enabled buttons, no spinners; + * running, mode unknown(-1) -> all three spin (module can't report the mode); + * running, mode 1/2/3 -> that slot spins, the other two are disabled buttons. + * Click listeners are static (set in initialize); a disabled button ignores taps. + */ + private void applyAcquireState() { + applyAcquireSlot(btnAcquireV1, progressAcquireV1, 1); + applyAcquireSlot(btnAcquireV2, progressAcquireV2, 2); + applyAcquireSlot(btnAcquireV3, progressAcquireV3, 3); + } + + private void applyAcquireSlot(View btn, View spinner, int mode) { + boolean spin = mainAcquiring && (mainAcquireMode < 0 || mainAcquireMode == mode); + btn.setVisibility(spin ? GONE : VISIBLE); + // Left enabled so a long-press always opens the info dialog; the short-press + // is gated in its click handler, and the icon+badge greys via alpha when it + // can't run (a run in flight, or no deficit / not loaded). + btn.setAlpha((!mainAcquiring && acquireEnabled) ? 1f : 0.38f); + spinner.setVisibility(spin ? VISIBLE : GONE); + } + + /** Long-press info: what the acquire mode does, with a Run/Cancel choice. */ + static void showAcquireInfo(@NonNull Context ctx, int mode, @NonNull Runnable onRun) { + int modeLabel = mode == 2 ? R.string.hugepage_proc_acquire_v2 + : mode == 3 ? R.string.hugepage_proc_acquire_v3 + : R.string.hugepage_proc_acquire_v1; + int msg = mode == 2 ? R.string.hugepage_acquire_v2_explain + : mode == 3 ? R.string.hugepage_acquire_v3_explain + : R.string.hugepage_acquire_v1_explain; + // Title = "Acquire huge pages" + the mode badge, e.g. "獲取大頁 v1". + String title = ctx.getString(R.string.hugepage_acquire_pages_title) + + " " + ctx.getString(modeLabel); + new MaterialAlertDialogBuilder(ctx) + .setTitle(title) + .setMessage(msg) + .setPositiveButton(R.string.hugepage_acquire_run, (d, w) -> onRun.run()) + .setNegativeButton(android.R.string.cancel, null) + .show(); + } + + /** + * Start filling the pool with acquire algorithm {@code mode} (see + * {@link HugePageModel#acquire}). Optimistically shows the spinners, fires the + * knob write on a dedicated thread (so the pool executor and its status poll + * stay free), and lets the periodic refresh reconcile the spinner from + * acquire_active. A failed trigger reverts immediately. + */ + private void startAcquire(int mode) { + if (mainAcquiring) return; // already running + Toast.makeText(this, R.string.hugepage_proc_acquire_running, LENGTH_SHORT).show(); + mainAcquiring = true; + mainAcquireMode = mode; // we know which we just started + applyAcquireState(); // immediate spinner feedback + new Thread(() -> { + var res = model.acquire(mode); + if (!res.ok()) { + mainAcquiring = false; + runOnUiThread(() -> { + Toast.makeText(this, R.string.hugepage_refill_failed, + LENGTH_SHORT).show(); + applyAcquireState(); + }); + return; + } + // Triggered: the periodic refresh reads acquire_active and keeps the + // spinner up until the worker clears it. This screen doesn't track + // completion, so if the mode degraded below what was asked, say so now. + final String actualImpl = res.impl != null ? res.impl : ""; + final boolean degraded = res.degraded; + runOnUiThread(() -> { + if (degraded) Toast.makeText(this, + getString(R.string.hugepage_acquire_degraded, actualImpl), + Toast.LENGTH_LONG).show(); + refreshStatus(); + }); + }, "hugepage-acquire").start(); + } + + /** Tapping an acquire spinner interrupts the run; the refresh clears the flag. */ + private void stopMainAcquire() { + runOnPool(() -> { + model.stopAcquire(); + runOnUiThread(this::refreshStatus); + }); + } + private void openModulePage() { var url = "https://github.com/Droid-VM/gh-hugepage-reserve/releases"; var intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); @@ -172,77 +295,26 @@ private void scheduleRefresh() { } private void refreshStatus() { - refreshStatus(false); - } - - /** - * @param forceCaps re-probe module capabilities immediately (bypass the - * cache) - pass true right after loading/unloading so the UI - * reflects the new module state without the cache's lag. - */ - private void refreshStatus(boolean forceCaps) { runOnPool(() -> { - var caps = model.caps(forceCaps); - var moduleInst = shellCheckExists(MODULE_PROP); - var moduleLoaded = caps.loaded; - var moduleDisabled = shellCheckExists(DISABLE_FILE); + // One version-unified read of module + pool state. + var snap = model.state(); var crashStamp = shellCheckExists(CRASH_FILE); - var refillStat = ""; - var servedSummary = ""; - var koAttribution = false; - if (moduleLoaded) try { - refillStat = shellReadFile(pathJoin(SYSFS_PARAMS, "refill_stat")); - // Reconcile + read per-VM served pages so this screen's bar shows - // the exact same per-VM breakdown as the usage screen's bar. - if (caps.koAttribution) { - run("echo 1 > %s/reconcile", SYSFS_PARAMS); - servedSummary = shellReadFile(pathJoin(SYSFS_PARAMS, "served_summary")); - koAttribution = true; + // Per-VM breakdown through the usage ladder (KO attribution, degrading + // to a THP scan of running VMs), so this bar matches the usage screen. + // Each segment is labelled with the friendly VM name from vmMap. + List owners = new ArrayList<>(); + if (snap.loaded) { + for (var e : model.usage(null).entries) { + if (e.pages > 0) owners.add(new long[]{e.pid, e.pages}); } - } catch (Exception e) { - Log.w(TAG, "Failed to read parameters", e); - } - var stats = parseProp(refillStat); - // pid -> friendly VM name, so each used segment is labelled "w11" - // like the usage screen. Old modules without the KO attribution API - // (no served_summary) fall back to a system THP scan of running VMs, - // mirroring the usage screen's "scan" mode, instead of showing the - // unavailable per-VM module-attribution breakdown. - List owners; - Map vmMap; - if (koAttribution) { - owners = parseServedOwners(servedSummary); - vmMap = owners.isEmpty() ? new LinkedHashMap<>() : model.vmNames(false); - } else { - vmMap = moduleLoaded ? model.vmNames(false) : new LinkedHashMap<>(); - owners = model.scanThpPages(vmMap.keySet()); } - runOnUiThread(() -> updateUI( - moduleInst, moduleLoaded, moduleDisabled, crashStamp, stats, owners, vmMap - )); + // Only fetch VM names when there are rows to label. + Map vmMap = owners.isEmpty() + ? new LinkedHashMap<>() : model.vmNames(false); + runOnUiThread(() -> updateUI(snap, crashStamp, owners, vmMap)); }); } - /** Parse served_summary "owner ... pid=N pages=M" lines into {pid, pages}. */ - @NonNull - private List parseServedOwners(@NonNull String raw) { - var owners = new ArrayList(); - for (var line : raw.split("\n")) { - line = line.trim(); - if (!line.startsWith("owner ")) continue; - long pid = -1, pages = 0; - for (var tok : line.split("\\s+")) { - try { - if (tok.startsWith("pid=")) pid = Long.parseLong(tok.substring(4)); - else if (tok.startsWith("pages=")) pages = Long.parseLong(tok.substring(6)); - } catch (NumberFormatException ignored) { - } - } - if (pid > 0 && pages > 0) owners.add(new long[]{pid, pages}); - } - return owners; - } - @NonNull private Map parseProp(@NonNull String raw) { var map = new LinkedHashMap(); @@ -258,86 +330,67 @@ private void setPagesString(@NonNull TextView tv, @StringRes int str, long pages tv.setText(getString(str, pages, SizeUtils.formatSize(pages * PAGE_SIZE))); } - private long getPages(@NonNull Map stats, @NonNull String key) { - var value = stats.get(key); - if (value == null) return 0; - return Long.parseLong(value); - } - private void updateUI( - boolean installed, boolean loaded, - boolean disabled, boolean crashed, - @NonNull Map stats, + @NonNull HugePageModel.Snapshot snap, boolean crashed, @NonNull List owners, @NonNull Map vmMap ) { if (isFinishing()) return; cardCrashWarning.setVisibility(crashed ? VISIBLE : GONE); - cardNotLoaded.setVisibility(loaded ? GONE : VISIBLE); - if (loaded && !stats.isEmpty()) { - rowStatState.setValue(stats.getOrDefault("state", "-")); - rowStatTotalServed.setValue(stats.getOrDefault("total_served", "-")); - rowStatTotalRefilled.setValue(stats.getOrDefault("total_refilled", "-")); - rowStatActiveVms.setValue(stats.getOrDefault("active_vms", "-")); - try { - var poolAvail = getPages(stats, "pool_avail"); - var poolTotal = getPages(stats, "pool_total"); - // "total" shows the desired target (pool_want); grow raises it - // while capacity (pool_total) only rises via acquire. Old - // modules without pool_want fall back to capacity. - var poolWant = getPages(stats, "pool_want"); - if (poolWant <= 0) poolWant = poolTotal; - // Apple-storage-bar style: one labelled colored block per VM - // (used), then the available portion as a track-coloured gap, - // then the waiting-to-acquire (deficit) block pinned flush right. - // Each block draws its label inside if wide enough. - boolean dark = HugePageColor.isDark(this); - int n = owners.size(); - int[] usedColors = new int[n]; - float[] usedValues = new float[n]; - String[] usedLabels = new String[n]; - long seg = 0; - for (int i = 0; i < n; i++) { - int pid = (int) owners.get(i)[0]; - long ownerPages = owners.get(i)[1]; - usedColors[i] = HugePageColor.forPid(pid, dark); - usedValues[i] = ownerPages; - String name = vmMap.get(pid); - if (name == null) name = getString(R.string.hugepage_proc_pid, pid); - // Two stacked lines: label over capacity. - usedLabels[i] = name + "\n" + SizeUtils.formatSize(ownerPages * PAGE_SIZE); - seg += ownerPages; - } - // "used" is the sum of the segments the bar draws, so the caption - // and the bar always agree (one canonical quantity: the per-VM - // served/scanned pages). The kernel 'served' counter can include - // orphaned owner-gone pages that have no segment; those surface in - // the held/available gap rather than as an invisible caption delta. - long used = seg; - long deficit = Math.max(0, poolWant - seg - poolAvail); - // 2x2 caption: used / available on top, total / pool-size below. - // Total = real held reserve (used + avail), shown raw - no clamp - // to the pool size, so a kernel that fails to release on shrink - // shows up as total > the size you set. - var held = used + poolAvail; - setPagesString(tvPoolUsed, R.string.hugepage_stat_pool_used, used); - setPagesString(tvPoolAvail, R.string.hugepage_stat_pool_available, poolAvail); - setPagesString(tvPoolTotal, R.string.hugepage_stat_pool_total, held); - setPagesString(tvPoolSize, R.string.hugepage_stat_pool_size, poolWant); - segPoolBar.setStorage(usedColors, usedValues, usedLabels, - poolAvail, getString(R.string.hugepage_bar_available) - + "\n" + SizeUtils.formatSize(poolAvail * PAGE_SIZE), - HugePageColor.pending(this), deficit, - getString(R.string.hugepage_proc_deficit) - + "\n" + SizeUtils.formatSize(deficit * PAGE_SIZE), - poolWant); - } catch (NumberFormatException e) { - tvPoolUsed.setText("-"); - tvPoolAvail.setText("-"); - tvPoolTotal.setText("-"); - tvPoolSize.setText("-"); - segPoolBar.setData(new int[0], new float[0], 0f); + cardNotLoaded.setVisibility(snap.loaded ? GONE : VISIBLE); + if (snap.loaded && snap.statsOk) { + rowStatState.setValue(snap.state); + rowStatTotalServed.setValue(snap.totalServed); + rowStatTotalRefilled.setValue(snap.totalRefilled); + rowStatActiveVms.setValue(snap.activeVms); + var poolAvail = snap.free; + // "total" shows the desired target - the model's version-unified + // want (pool_want, else v6 pool_target, else current capacity). + var poolWant = snap.targetIdeal; + // Apple-storage-bar style: one labelled colored block per VM + // (used), then the available portion as a track-coloured gap, + // then the waiting-to-acquire (deficit) block pinned flush right. + // Each block draws its label inside if wide enough. + boolean dark = HugePageColor.isDark(this); + int n = owners.size(); + int[] usedColors = new int[n]; + float[] usedValues = new float[n]; + String[] usedLabels = new String[n]; + long seg = 0; + for (int i = 0; i < n; i++) { + int pid = (int) owners.get(i)[0]; + long ownerPages = owners.get(i)[1]; + usedColors[i] = HugePageColor.forPid(pid, dark); + usedValues[i] = ownerPages; + String name = vmMap.get(pid); + if (name == null) name = getString(R.string.hugepage_proc_pid, pid); + // Two stacked lines: label over capacity. + usedLabels[i] = name + "\n" + SizeUtils.formatSize(ownerPages * PAGE_SIZE); + seg += ownerPages; } + // "used" is the sum of the segments the bar draws, so the caption + // and the bar always agree (one canonical quantity: the per-VM + // served/scanned pages). The kernel 'served' counter can include + // orphaned owner-gone pages that have no segment; those surface in + // the held/available gap rather than as an invisible caption delta. + long used = seg; + long deficit = Math.max(0, poolWant - seg - poolAvail); + // 2x2 caption: used / available on top, total / pool-size below. + // Total = real held reserve (used + avail), shown raw - no clamp + // to the pool size, so a kernel that fails to release on shrink + // shows up as total > the size you set. + var held = used + poolAvail; + setPagesString(tvPoolUsed, R.string.hugepage_stat_pool_used, used); + setPagesString(tvPoolAvail, R.string.hugepage_stat_pool_available, poolAvail); + setPagesString(tvPoolTotal, R.string.hugepage_stat_pool_total, held); + setPagesString(tvPoolSize, R.string.hugepage_stat_pool_size, poolWant); + segPoolBar.setStorage(usedColors, usedValues, usedLabels, + poolAvail, getString(R.string.hugepage_bar_available) + + "\n" + SizeUtils.formatSize(poolAvail * PAGE_SIZE), + HugePageColor.pending(this), deficit, + getString(R.string.hugepage_proc_deficit) + + "\n" + SizeUtils.formatSize(deficit * PAGE_SIZE), + poolWant); } else { rowStatState.setValue(getString(R.string.hugepage_stats_unavailable)); rowStatTotalServed.setValue(null); @@ -353,85 +406,102 @@ poolAvail, getString(R.string.hugepage_bar_available) // floppy icon kept) but stays pressable: tapping it then interrupts the // acquire, keeping the current size (see the click handler). pool_want is // settable any time now, so neither button needs to be disabled. - boolean acquiring = loaded && "1".equals(stats.get("acquire_active")); + boolean acquiring = snap.acquiring; moduleAcquiring = acquiring; + // Reconcile the acquire slots from acquire_active + acquire_mode so a run + // that finishes (or was started elsewhere) toggles spinners<->buttons, and + // only the running mode spins. acquire_mode is -1 on old modules -> makes + // all three spin. + int mode = acquiring ? snap.acquireMode : -1; + // A real acquire_active 1 -> 0 edge = the worker finished; announce the + // achieved size (this screen is fire-and-forget - unlike the process list, + // which polls). Track the kernel flag, not the optimistic mainAcquiring, so + // an acquire that never actually started can't fake a "done". + if (wasAcquiring && !acquiring) { + long got = snap.free + snap.lent; + long want = snap.targetIdeal; + Toast.makeText(this, got >= want + ? getString(R.string.hugepage_proc_acquire_full, + SizeUtils.formatSize(want * PAGE_SIZE)) + : getString(R.string.hugepage_proc_acquire_partial, + SizeUtils.formatSize(got * PAGE_SIZE), + SizeUtils.formatSize(want * PAGE_SIZE)), + Toast.LENGTH_LONG).show(); + } + wasAcquiring = acquiring; + if (mainAcquiring != acquiring || mainAcquireMode != mode) { + mainAcquiring = acquiring; + mainAcquireMode = mode; + applyAcquireState(); + } progressSavePoolSize.setVisibility(acquiring ? VISIBLE : GONE); if (acquiring) btnSavePoolSize.setTextColor(Color.TRANSPARENT); else btnSavePoolSize.setTextColor(saveTextColors); - btnSavePoolSize.setEnabled(installed); + btnSavePoolSize.setEnabled(snap.installed); // Install / Enable / Disable. "Disable" on v7 shrinks the pool to one // page (frees memory) but keeps the module loaded so it never loses // per-VM tracking; soft-disabled shows "Enable" again. v6 has no // pool_want knob, so Disable falls back to rmmod. - moduleInstalled = installed; - moduleLoaded = loaded; - moduleHasPoolWant = stats.containsKey("pool_want"); - moduleSoftDisabled = loaded && moduleHasPoolWant - && getPages(stats, "pool_want") <= 1; + moduleInstalled = snap.installed; + moduleLoaded = snap.loaded; + moduleHasPoolWant = snap.hasPoolWant; + moduleSoftDisabled = snap.softDisabled; btnModuleToggle.setEnabled(true); - if (!installed) { + if (!snap.installed) { btnModuleToggle.setText(R.string.hugepage_btn_install); btnModuleToggle.setIconResource(R.drawable.ic_download); - } else if (!loaded || moduleSoftDisabled) { + } else if (!snap.loaded || snap.softDisabled) { btnModuleToggle.setText(R.string.hugepage_btn_enable); btnModuleToggle.setIconResource(R.drawable.ic_start); } else { btnModuleToggle.setText(R.string.hugepage_btn_disable); btnModuleToggle.setIconResource(R.drawable.ic_stop); } - rowModuleEnable.setEnabled(installed); - rowModuleEnable.setChecked(!disabled); + rowModuleEnable.setEnabled(snap.installed); + rowModuleEnable.setChecked(snap.bootEnabled); + + // Acquire buttons usable exactly when there is a deficit to fill. The + // model's version-unified deficit already folds in v6 (no pool_want) and + // soft-disable (want 0), so there is no version branching here. + acquireEnabled = snap.loaded && snap.deficit > 0; + applyAcquireState(); } - private void doLoad() { + /** + * Bring the pool up: load the module (if unloaded) or restore the saved target + * (if soft-disabled), then kick one gentle v1 fill toward it. The model picks + * the version-appropriate path (insmod / pool_want write) and reads the size + * from settings.prop itself. + */ + private void doEnable() { btnModuleToggle.setEnabled(false); runOnPool(() -> { - if (shellCheckExists(SYSFS_BASE)) { - runOnUiThread(() -> { - Toast.makeText(this, R.string.hugepage_already_loaded, - LENGTH_SHORT).show(); - refreshStatus(true); - }); - return; - } - // Read the configured size from settings.prop in Java (rather than - // sourcing it in shell). v7 reads pool_want, older builds pool_target. - var size = "1024"; - if (shellCheckExists(SETTINGS_PROP)) { - var settings = parseProp(shellReadFile(SETTINGS_PROP)); - size = settings.getOrDefault("pool_want", - settings.getOrDefault("pool_target", "1024")); - } - // Insmod compatibly across versions: v7 takes pool_want, older builds - // take pool_target, anything else loads bare. First success wins. - var ko = pathJoin(MAGISK_BASE, "gh_hugepage_reserve.ko"); - var result = - run("insmod \"%s\" pool_want=\"%s\"", ko, size).isSuccess() - || run("insmod \"%s\" pool_target=\"%s\"", ko, size).isSuccess() - || run("insmod \"%s\"", ko).isSuccess(); + var res = model.setRuntimeEnabled(true); + if (res.ok()) model.acquire(1); // GUI drives the fill (v1 = gentlest) runOnUiThread(() -> { - Toast.makeText(this, result - ? R.string.hugepage_loaded - : R.string.hugepage_load_failed, + Toast.makeText(this, res.ok() + ? R.string.hugepage_loaded : R.string.hugepage_load_failed, LENGTH_SHORT).show(); - refreshStatus(true); + refreshStatus(); }); }); } /** - * Soft-disable (v7): shrink the pool to a single page so the reserved memory - * is freed back to the system, but keep the module loaded so it never loses - * per-VM tracking (unlike rmmod). settings.prop is left untouched, so the - * next boot still allocates the configured capacity. + * Take the pool down: soft-disable (shrink pool_want to 0, module stays loaded + * so per-VM tracking survives) where supported, else rmmod (v6). The module + * refuses a resize mid-acquire, so the running worker is stopped first. */ - private void doSoftDisable() { + private void doDisable() { btnModuleToggle.setEnabled(false); runOnPool(() -> { - // The module refuses a resize mid-acquire, so stop the worker first. stopAcquireAndWait(); - run("echo 1 > %s/pool_want", SYSFS_PARAMS); - runOnUiThread(this::refreshStatus); + var res = model.setRuntimeEnabled(false); + runOnUiThread(() -> { + if (!res.ok()) Toast.makeText(this, + R.string.hugepage_unload_failed, LENGTH_SHORT).show(); + refreshStatus(); + }); }); } @@ -442,7 +512,7 @@ private void doSoftDisable() { */ private void interruptAcquire() { runOnPool(() -> { - run("echo 0 > %s/acquire", SYSFS_PARAMS); + model.stopAcquire(); runOnUiThread(this::refreshStatus); }); } @@ -450,15 +520,14 @@ private void interruptAcquire() { /** * Interrupt any running acquire and block (on the pool thread) until the * worker is quiescent. pool_want can't be set while an acquire runs, so call - * this before writing pool_want. The worker can be mid-migration, so this may - * take a moment; bounded so a wedged worker can't hang us forever. + * this before a soft-disable. The worker can be mid-migration, so this may take + * a moment; bounded so a wedged worker can't hang us forever. */ private void stopAcquireAndWait() { - if (!shellCheckExists(pathJoin(SYSFS_PARAMS, "acquire"))) return; - run("echo 0 > %s/acquire", SYSFS_PARAMS); + if (!model.acquiring()) return; // nothing running (also the v6 case) + model.stopAcquire(); for (int i = 0; i < 60; i++) { - var st = parseProp(shellReadFile(pathJoin(SYSFS_PARAMS, "refill_stat"))); - if (!"1".equals(st.get("acquire_active"))) return; + if (!model.acquiring()) return; try { Thread.sleep(500); } catch (InterruptedException ignored) { @@ -467,24 +536,6 @@ private void stopAcquireAndWait() { } } - /** Re-enable a soft-disabled pool: restore the configured size and refill. */ - private void doSoftEnable() { - btnModuleToggle.setEnabled(false); - runOnPool(() -> { - var size = "1024"; - if (shellCheckExists(SETTINGS_PROP)) { - var s = parseProp(shellReadFile(SETTINGS_PROP)); - size = s.getOrDefault("pool_want", - s.getOrDefault("pool_target", "1024")); - } - run("echo %s > %s/pool_want", size, SYSFS_PARAMS); - if (shellCheckExists(pathJoin(SYSFS_PARAMS, "acquire"))) { - run("echo 1 > %s/acquire", SYSFS_PARAMS); - } - runOnUiThread(this::refreshStatus); - }); - } - private void loadPoolSize() { runOnPool(() -> { Map settings; @@ -562,32 +613,19 @@ private void savePoolSize() { LENGTH_SHORT).show()); return; } - // Persist for the next load. Write BOTH keys so the size survives - // whichever module/loader is installed at boot: v7's loader reads - // pool_want, an older (v6) package's loader reads pool_target. - var wroteWant = run("echo 'pool_want=%s' > %s", - pages.toString(), SETTINGS_PROP).isSuccess(); - var wroteTarget = run("echo 'pool_target=%s' >> %s", - pages.toString(), SETTINGS_PROP).isSuccess(); - var result = wroteWant && wroteTarget; - // Apply at runtime via the single pool_want knob: shrink frees excess - // now, grow only raises the target (Acquire fills it). Old modules - // without pool_want just keep the saved value (effective next reboot). - var applied = false; - if (shellCheckExists(pathJoin(SYSFS_PARAMS, "pool_want"))) { - applied = run("echo %s > %s/pool_want", - pages.toString(), SYSFS_PARAMS).isSuccess(); - // Saving also kicks off one acquire so a grown target starts - // filling immediately (a shrink just makes the worker exit at - // once). The 1 s status poll then animates the bar + Save spinner. - if (applied && shellCheckExists(pathJoin(SYSFS_PARAMS, "acquire"))) { - run("echo 1 > %s/acquire", SYSFS_PARAMS); - } - } - var finalApplied = applied; + // Persist for the next load and apply to the running pool where the + // live knob exists (v7); v6's read-only target only lands next boot. + var res = model.saveSize(pages.longValue()); + // A grow leaves a deficit -> kick one v1 fill so the raised target + // starts filling at once (a shrink is applied by the write itself). + // The GUI drives acquire; the model's saveSize deliberately doesn't. + var snap = model.state(); + if (res.ok() && snap.loaded && snap.deficit > 0) model.acquire(1); + boolean okSaved = res.ok(); + boolean appliedNow = "pool_want".equals(res.impl); // live write actually landed runOnUiThread(() -> { - int msg = !result ? R.string.hugepage_pool_size_failed - : finalApplied ? R.string.hugepage_pool_size_applied + int msg = !okSaved ? R.string.hugepage_pool_size_failed + : appliedNow ? R.string.hugepage_pool_size_applied : R.string.hugepage_pool_size_saved; Toast.makeText(this, msg, LENGTH_SHORT).show(); refreshStatus(); @@ -597,13 +635,11 @@ private void savePoolSize() { private void doToggleModule() { var enabled = rowModuleEnable.isChecked(); - if (shellCheckExists(DISABLE_FILE) != enabled) return; + if (shellCheckExists(DISABLE_FILE) != enabled) return; // ignore the programmatic echo runOnPool(() -> { - var result = enabled ? - runList("rm", "-f", DISABLE_FILE) : - runList("touch", DISABLE_FILE); + var res = model.setBootEnabled(enabled); runOnUiThread(() -> { - if (result.isSuccess()) { + if (res.ok()) { var msg = enabled ? R.string.hugepage_module_now_enabled : R.string.hugepage_module_now_disabled; @@ -618,25 +654,11 @@ private void confirmUnload() { new MaterialAlertDialogBuilder(this) .setTitle(R.string.hugepage_stop) .setMessage(R.string.hugepage_stop_confirm) - .setPositiveButton(R.string.hugepage_stop, (d, w) -> doUnload()) + .setPositiveButton(R.string.hugepage_stop, (d, w) -> doDisable()) .setNegativeButton(android.R.string.cancel, null) .show(); } - private void doUnload() { - btnModuleToggle.setEnabled(false); - runOnPool(() -> { - var result = runList("rmmod", "gh_hugepage_reserve").isSuccess(); - runOnUiThread(() -> { - var msg = result ? - R.string.hugepage_unloaded : - R.string.hugepage_unload_failed; - Toast.makeText(this, msg, LENGTH_SHORT).show(); - refreshStatus(true); - }); - }); - } - private void doDismissCrash() { runOnPool(() -> { runList("rm", "-f", CRASH_FILE); diff --git a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageModel.java b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageModel.java index 0e1958c42..8c4b5e9ca 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageModel.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageModel.java @@ -3,6 +3,7 @@ import static cn.classfun.droidvm.lib.utils.FileUtils.shellCheckExists; import static cn.classfun.droidvm.lib.utils.FileUtils.shellReadFile; import static cn.classfun.droidvm.lib.utils.RunUtils.run; +import static cn.classfun.droidvm.lib.utils.RunUtils.runList; import static cn.classfun.droidvm.lib.utils.StringUtils.pathJoin; import android.os.SystemClock; @@ -18,131 +19,679 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; import java.util.regex.Pattern; import cn.classfun.droidvm.lib.daemon.DaemonConnection; +import cn.classfun.droidvm.lib.run.RunResult; /** - * Shared huge-page data layer for the status ({@link HugePageActivity}) and - * process ({@link HugePageProcessActivity}) screens. + * The single huge-page facade both screens ({@link HugePageActivity}, + * {@link HugePageProcessActivity}) talk to. * - *

Both screens run a per-second refresh loop, so this class caches the three - * expensive sources with short TTLs so a refresh no longer re-probes root sysfs, - * blocks on the daemon, or re-scans {@code /proc} every single tick: - *

    - *
  • capabilities — one batched root probe of the module's sysfs knobs, - * resolved at most once per {@link #CAPS_TTL_MS} (a static property of the - * loaded {@code .ko}, not something to re-derive every tick);
  • - *
  • VM names — the daemon {@code vm_list} pid→name map, fetched at - * most once per {@link #VM_TTL_MS} and reused (last-known) on a slow daemon - * so labels never blank to raw pids;
  • - *
  • pool_target — the read-only v6 grow target, read once and cached.
  • - *
- * Each screen owns its own instance (separate caches); everything here is - * context-free (pure data), so it never holds an Activity reference. + *

It hides the {@code gh_hugepage_reserve} module's v6/v7 differences: + * callers ask for a high-level operation and never branch on version or poke raw + * sysfs. Version is never decided ahead of time — each mutation is a + * degradation ladder that just tries the best knob and falls back + * (e.g. the migrating {@code acquire} knob → {@code manual_refill}; + * {@code pool_want=0} soft-disable → {@code rmmod}). The internal {@link Try} + * ladder is collapsed to a small {@link Result} ({@code OK / UNSUPPORTED / + * FAILED}) for the GUI — that is the "無法執行就回傳 error" contract. + * + *

Reads are cached with short TTLs so the per-second refresh loop doesn't + * block on the daemon or re-read a static knob every tick. Each screen owns its + * own instance (separate caches); everything here is context-free (pure data) and + * never holds an Activity reference. Mutations do shell I/O — call them off the UI + * thread. */ final class HugePageModel { static final String SYSFS_BASE = "/sys/module/gh_hugepage_reserve"; static final String SYSFS_PARAMS = pathJoin(SYSFS_BASE, "parameters"); - static final String SYSFS_OWNERS = pathJoin(SYSFS_PARAMS, "vm_owners"); - /** A THP huge page is 2 MiB, i.e. 2048 KiB. */ - static final long KB_PER_PAGE = 2048; - static final long PAGE_BYTES = KB_PER_PAGE * 1024; + private static final String SYSFS_OWNERS = pathJoin(SYSFS_PARAMS, "vm_owners"); + private static final String MAGISK_BASE = "/data/adb/modules/gh-hugepage-reserve"; + private static final String MODULE_PROP = pathJoin(MAGISK_BASE, "module.prop"); + private static final String DISABLE_FILE = pathJoin(MAGISK_BASE, "disable"); + private static final String SETTINGS_PROP = pathJoin(MAGISK_BASE, "settings.prop"); + private static final String KO_PATH = pathJoin(MAGISK_BASE, "gh_hugepage_reserve.ko"); + /** A THP huge page is 2 MiB = 2048 KiB. */ + private static final long KB_PER_PAGE = 2048; + /** Default target if settings.prop has none (pages): 1024 x 2 MB = 2 GB. */ + private static final long DEFAULT_WANT_PAGES = 1024; /** meminfo/smaps values are " kB"; strip everything but the digits once. */ private static final Pattern NON_DIGITS = Pattern.compile("[^0-9]"); - private static final long CAPS_TTL_MS = 3000; + private static final long STAT_TTL_MS = 3000; private static final long VM_TTL_MS = 4000; private static final long DAEMON_TIMEOUT_S = 2; /* ================================================================== */ - /* Module capabilities */ + /* Public result / data types */ /* ================================================================== */ - /** Immutable snapshot of what the currently-loaded module supports. */ - static final class Caps { - /** {@code /sys/module/gh_hugepage_reserve} exists (module is loaded). */ - final boolean loaded; - /** served_summary present: per-VM KO attribution API (v7). */ - final boolean koAttribution; - /** pool_want knob present (v7 resizable target). */ - final boolean hasPoolWant; - /** acquire knob present (v7 async grow). */ - final boolean hasAcquire; - /** manual_refill knob present (v6 refill). */ - final boolean hasManualRefill; - - Caps(boolean loaded, boolean koAttribution, boolean hasPoolWant, - boolean hasAcquire, boolean hasManualRefill) { + /** Outcome of a mutation. {@code UNSUPPORTED} = the module/version can't do it. */ + enum Outcome { OK, UNSUPPORTED, FAILED } + + /** Which way a usage list was sourced: module KO attribution, or a THP scan. */ + enum Source { KO, SCAN } + + /** + * Collapsed result of a high-level mutation: the overall outcome, which + * concrete implementation won (or last failed), and a human reason on failure. + */ + static final class Result { + @NonNull final Outcome outcome; + /** Which rung ran (e.g. "acquire", "manual_refill", "soft", "BARE"), or null. */ + @Nullable final String impl; + /** Failure reason, or null on success. */ + @Nullable final String detail; + /** + * True when a fallback rung won: a preferred implementation was + * tried first and failed. This replaces the old ladder's "the trace has + * more than one element" degradation signal - {@link #impl} says which + * fallback, {@code degraded} says that it is one. + */ + final boolean degraded; + + private Result(@NonNull Outcome outcome, @Nullable String impl, + @Nullable String detail, boolean degraded) { + this.outcome = outcome; + this.impl = impl; + this.detail = detail; + this.degraded = degraded; + } + + boolean ok() { + return outcome == Outcome.OK; + } + + static Result ok(@Nullable String impl) { + return new Result(Outcome.OK, impl, null, false); + } + + static Result ok(@Nullable String impl, boolean degraded) { + return new Result(Outcome.OK, impl, null, degraded); + } + + static Result failed(@Nullable String impl, @Nullable String detail) { + return new Result(Outcome.FAILED, impl, detail, false); + } + + static Result unsupported(@Nullable String detail) { + return new Result(Outcome.UNSUPPORTED, null, detail, false); + } + } + + /** A per-VM usage list plus which source produced it. */ + static final class Usage { + @NonNull final List entries; + @NonNull final Source source; + + Usage(@NonNull List entries, @NonNull Source source) { + this.entries = entries; + this.source = source; + } + } + + /** One attributed process: pid, name, huge pages, and live /proc state. */ + static final class UsageEntry { + final int pid; + /** Owner comm (KO) or VM name (scan). */ + @NonNull final String comm; + /** Huge pages attributed: reconciled served count (KO) or THP occupancy (scan). */ + final long pages; + /** /proc run state (R/S/D/...), or '?' if unknown. */ + final char state; + final boolean alive; + + UsageEntry(int pid, @NonNull String comm, long pages, char state, boolean alive) { + this.pid = pid; + this.comm = comm; + this.pages = pages; + this.state = state; + this.alive = alive; + } + } + + /** + * Version-unified snapshot of module + pool state. {@code targetIdeal} is the + * one "target" concept across versions: v7's {@code pool_want}, else v6's + * read-only {@code pool_target}, else current capacity. {@code deficit} is what + * is still missing toward it ({@code targetIdeal − free − lent}); the acquire + * buttons are usable exactly when {@code loaded && deficit > 0}. + */ + static final class Snapshot { + final boolean installed; // Magisk module files present + final boolean loaded; // insmod'd (sysfs node exists) + final boolean statsOk; // refill_stat was readable (false on a transient failure) + final boolean bootEnabled; // will load next boot (no Magisk disable file) + final long targetIdeal; // unified target (want): pool_want ?? pool_target ?? built + final long built; // pool_total (capacity assembled) + final long free; // pool_avail (in the pool now) + final long lent; // served (out to VMs); 0 if the module can't report it (v6) + final long deficit; // max(0, targetIdeal − free − lent) + final boolean acquiring; // an acquire worker is running + final int acquireMode; // which mode (1/2/3), or -1 if the module can't report it + final boolean hasPoolWant; // pool_want knob reported (v7 runtime-resizable) + final boolean softDisabled; // v7 pool_want <= 1 (reserve released, module stays loaded) + // Raw display strings from refill_stat (pass-through, "-" when absent). + @NonNull final String state; + @NonNull final String totalServed; + @NonNull final String totalRefilled; + @NonNull final String activeVms; + + private Snapshot(boolean installed, boolean loaded, boolean statsOk, boolean bootEnabled, + long targetIdeal, long built, long free, long lent, long deficit, + boolean acquiring, int acquireMode, boolean hasPoolWant, + boolean softDisabled, @NonNull String state, @NonNull String totalServed, + @NonNull String totalRefilled, @NonNull String activeVms) { + this.installed = installed; this.loaded = loaded; - this.koAttribution = koAttribution; + this.statsOk = statsOk; + this.bootEnabled = bootEnabled; + this.targetIdeal = targetIdeal; + this.built = built; + this.free = free; + this.lent = lent; + this.deficit = deficit; + this.acquiring = acquiring; + this.acquireMode = acquireMode; this.hasPoolWant = hasPoolWant; - this.hasAcquire = hasAcquire; - this.hasManualRefill = hasManualRefill; + this.softDisabled = softDisabled; + this.state = state; + this.totalServed = totalServed; + this.totalRefilled = totalRefilled; + this.activeVms = activeVms; } } - @Nullable - private volatile Caps capsCache; - private volatile long capsAtMs = Long.MIN_VALUE; + /* ================================================================== */ + /* Public high-level API */ + /* ================================================================== */ /** - * Module capabilities, cached for {@link #CAPS_TTL_MS}. Resolving this from a - * cache (instead of re-probing every refresh) is what stops a single flaky - * root {@code test -e} from flapping the whole view between KO and scan mode. - * Pass {@code force = true} right after an action that changes the module - * (load / unload / soft-disable) to re-probe immediately. + * Version-unified module + pool snapshot. Never throws; when the module isn't + * loaded the pool fields are zero and {@code loaded == false}. */ @NonNull - Caps caps(boolean force) { - long now = SystemClock.elapsedRealtime(); - var c = capsCache; - if (!force && c != null && now - capsAtMs < CAPS_TTL_MS) return c; - c = probeCaps(); - capsCache = c; - capsAtMs = now; - return c; + Snapshot state() { + boolean installed = existsSticky(MODULE_PROP); + boolean bootEnabled = !shellCheckExists(DISABLE_FILE); + boolean loaded = existsSticky(SYSFS_BASE); + if (!loaded) { + return new Snapshot(installed, false, false, bootEnabled, + 0, 0, 0, 0, 0, false, -1, false, false, "-", "-", "-", "-"); + } + var s = parseProp(safeRead(pathJoin(SYSFS_PARAMS, "refill_stat"))); + boolean statsOk = !s.isEmpty(); // empty == the read failed transiently + boolean hasWant = s.containsKey("pool_want"); + long built = getLong(s, "pool_total", 0); + long free = getLong(s, "pool_avail", 0); + long lent = getLong(s, "served", 0); + long rawWant = getLong(s, "pool_want", -1); + long want = rawWant; + if (want < 0) want = poolTarget(); // v6: separate read-only knob + if (want < 0) want = built; // last resort: current capacity + long deficit = Math.max(0, want - free - lent); + boolean acquiring = "1".equals(s.get("acquire_active")); + int mode = (int) getLong(s, "acquire_mode", -1); + boolean softDisabled = hasWant && rawWant <= 1; + return new Snapshot(installed, true, statsOk, bootEnabled, + want, built, free, lent, deficit, acquiring, mode, hasWant, softDisabled, + s.getOrDefault("state", "-"), s.getOrDefault("total_served", "-"), + s.getOrDefault("total_refilled", "-"), s.getOrDefault("active_vms", "-")); } /** - * One batched root round-trip that tests every knob at once (5 {@code test -e} - * in a single {@code run}, instead of one exec per knob). If it comes back - * "not loaded" we retry once, since a lone root {@code test -e} can flake - * under load and we don't want to spuriously report the module gone. + * Whether an acquire worker is currently running. Reads only {@code refill_stat} + * (no module / boot-file probing), so it is cheap enough for the tight poll + * loops where a full {@link #state()} every 500 ms would be wasteful. + */ + boolean acquiring() { + return "1".equals(parseProp(safeRead( + pathJoin(SYSFS_PARAMS, "refill_stat"))).get("acquire_active")); + } + + /** + * Per-VM huge-page usage. Prefers KO attribution (the module's reconciled + * {@code served_summary} + {@code vm_owners}); degrades to a THP scan of the + * daemon's running VMs. Pass a {@code pref} to pin a source (the UI lets the + * user choose), or {@code null} to walk the ladder. */ @NonNull - private Caps probeCaps() { - var caps = probeCapsOnce(); - if (!caps.loaded) caps = probeCapsOnce(); // absorb a flaky probe - return caps; + Usage usage(@Nullable Source pref) { + var log = new ArrayList>>(); + if (rung(pref, log, Source.KO, this::usageFromKo)) return usageOf(log); + rung(pref, log, Source.SCAN, this::usageFromScan); + return usageOf(log); } + /** Whether KO attribution is currently available (the {@code served_summary} knob exists). */ + boolean koAvailable() { + return existsSticky(pathJoin(SYSFS_PARAMS, "served_summary")); + } + + /** + * Save a new pool target of {@code pages}. Persists it to settings.prop (for + * the next boot) and applies it to the running pool if the live {@code pool_want} + * knob exists; on v6 (read-only {@code pool_target}) only the persist takes + * effect. Does not fire an acquire — the caller drives that from the + * resulting {@link Snapshot#deficit}. + */ @NonNull - private Caps probeCapsOnce() { - String raw; + Result saveSize(long pages) { + var persisted = writeSettings(pages); + if (!persisted.ok()) return Result.failed("settings", persisted.error); + // Best-effort live apply; the impl reports whether it actually took effect + // on the running pool ("pool_want") or only persisted for next boot + // ("settings" - v6's read-only target, or a rejected write). + var live = writeKnob("pool_want", Long.toString(pages)); + // Degraded when the live write didn't land (v6 read-only target, or a + // rejected write): only the next-boot persist took effect. + return Result.ok(live.ok() ? "pool_want" : "settings", !live.ok()); + } + + /** + * Bring the running pool up (true) or down (false), choosing the deepest action + * the module supports: + *

    + *
  • enable: not loaded → insmod (targeting the saved size); loaded → + * restore the target by writing the saved {@code pool_want} (v6 loaded is + * already active, so this is a no-op there);
  • + *
  • disable: try {@code pool_want=0} (soft — frees the reserve but + * keeps per-VM tracking); if that write can't happen (v6 has no such knob) + * fall back to {@code rmmod}.
  • + *
+ */ + @NonNull + Result setRuntimeEnabled(boolean on) { + if (on) { + if (!existsSticky(SYSFS_BASE)) { + return collapse(loadLadder(savedWantPages())); // insmod + } + // Loaded: restore the target. Succeeds on v7; on v6 there's no pool_want + // and a loaded module is already active, so treat a failed write as "已啟用". + var t = writeKnob("pool_want", Long.toString(savedWantPages())); + return t.ok() ? Result.ok("soft-enable") : Result.ok("already-active"); + } + // Disable. Pick soft-vs-rmmod by *capability* (does the pool_want knob + // exist?), NOT by write success: a v7 soft-disable can also fail + // transiently (the module refuses a resize mid-acquire, or a root hiccup), + // and escalating that to rmmod would destroy the per-VM tracking that + // soft-disable exists to preserve. So v7 only ever soft-disables (a failed + // write is reported as FAILED to retry); only v6 (no knob) falls to rmmod. + if (existsSticky(pathJoin(SYSFS_PARAMS, "pool_want"))) { + var soft = writeKnob("pool_want", "0"); + return soft.ok() ? Result.ok("soft") : Result.failed("soft", soft.error); + } + return collapse(unloadLadder()); + } + + /** + * Enable (true) or disable (false) the module for the next boot via the + * Magisk {@code disable} marker file. Independent of the running module. + */ + @NonNull + Result setBootEnabled(boolean on) { + var r = on + ? runList("rm", "-f", DISABLE_FILE) + : run("touch %s", DISABLE_FILE); + return r.isSuccess() + ? Result.ok(on ? "enable-boot" : "disable-boot") + : Result.failed("disable-file", reason(r, "disable")); + } + + /** + * Fill the pool toward its target with acquire algorithm {@code mode} + * (1 = original scan, 2 = migrate + system reclaim, 3 = migrate + per-block + * evict). Prefers the migrating {@code acquire} knob; on any failure (v6 has no + * such knob, or -ENOSYS where this kernel can't run that mode) degrades to + * {@code manual_refill}. Fire-and-return — poll {@link #state()} for completion. + */ + @NonNull + Result acquire(int mode) { + // Drop the whole VM page cache + reclaimable slab first so more 2 MB windows + // are already free (or trivially assemblable) before the sweep - the module's + // own acquire_drop_slab covers slab only, not the page cache. Best-effort: a + // failed drop doesn't block the acquire. + run("echo 3 > /proc/sys/vm/drop_caches"); + // Walk the requested mode down to the gentlest migrating mode: the module + // returns -ENOSYS for a mode whose symbols didn't resolve (e.g. v3 on a + // kernel lacking the folio/reclaim symbols, or v2 with no alloc_contig_range), + // so try mode -> ... -> 1 before giving up on migration and falling back to a + // plain manual_refill. Any acquire mode is pollable via acquire_active, so the + // winning impl is uniformly "acquire" (the running mode surfaces in acquire_mode). + for (int m = mode; m >= 1; m--) { + if (writeKnob("acquire", Integer.toString(m)).ok()) { + // impl names the actual migrating mode that ran ("v1".."v3"); + // degraded when it's below what was asked for. + return Result.ok("v" + m, m != mode); + } + } + var refill = writeKnob("manual_refill", "1"); + return refill.ok() + ? Result.ok("manual_refill", true) + : Result.unsupported("no acquire / manual_refill knob"); + } + + /** Interrupt a running acquire (write 0 to the knob). No worker exists on v6. */ + @NonNull + Result stopAcquire() { + var t = writeKnob("acquire", "0"); + return t.ok() ? Result.ok("acquire") : Result.unsupported(t.error); + } + + /* ================================================================== */ + /* Ladder plumbing (internal) */ + /* ================================================================== */ + + /** insmod ladder: both keys, then {@code pool_want=} (v7), {@code pool_target=} (v6), bare. */ + private enum LoadImpl { BOTH, POOL_WANT, POOL_TARGET, BARE } + + /** Marker for single-implementation actions. */ + private enum Only { DEFAULT } + + private static boolean rung( + @Nullable I only, @NonNull List> log, + @NonNull I impl, @NonNull Supplier> action + ) { + if (only != null && only != impl) return false; + var t = action.get(); + log.add(t); + return t.ok(); + } + + /** Collapse a finished ladder trace into a {@link Result}. */ + @NonNull + private static Result collapse(@NonNull List> log) { + if (log.isEmpty()) return Result.failed(null, "no attempt"); + var last = log.get(log.size() - 1); + boolean degraded = log.size() > 1; // earlier rungs failed before this won + return last.ok() + ? Result.ok(String.valueOf(last.impl), degraded) + : Result.failed(String.valueOf(last.impl), last.error); + } + + @NonNull + private static Usage usageOf(@NonNull List>> log) { + var last = log.isEmpty() ? null : log.get(log.size() - 1); + if (last != null && last.ok() && last.value != null) { + return new Usage(last.value, last.impl); + } + return new Usage(new ArrayList<>(), Source.SCAN); + } + + /* ================================================================== */ + /* Module lifecycle ladders */ + /* ================================================================== */ + + @NonNull + private List> loadLadder(long pages) { + var log = new ArrayList>(); + var w = "pool_want=\"" + pages + "\""; + var t = "pool_target=\"" + pages + "\""; + // Pass BOTH size params first. A lenient kernel silently ignores the param + // the module doesn't have, so it still gets the right size via the one it + // does - pool_want (v7) or pool_target (v6). This is essential: on a v6 + // module `pool_want=` alone loads fine (returns 0) but is ignored, leaving + // the pool at its compiled 1024 default - the ladder must not stop there. + // Strict kernels reject the unknown param, so fall back to each key alone, + // then a bare (default-size) load. + if (rung(null, log, LoadImpl.BOTH, + () -> insmod(LoadImpl.BOTH, w + " " + t))) return log; + if (rung(null, log, LoadImpl.POOL_WANT, + () -> insmod(LoadImpl.POOL_WANT, w))) return log; + if (rung(null, log, LoadImpl.POOL_TARGET, + () -> insmod(LoadImpl.POOL_TARGET, t))) return log; + rung(null, log, LoadImpl.BARE, () -> insmod(LoadImpl.BARE, null)); + return log; + } + + private static Try insmod(@NonNull LoadImpl impl, @Nullable String arg) { + var r = (arg == null) + ? run("insmod \"%s\"", KO_PATH) + : run("insmod \"%s\" %s", KO_PATH, arg); + return r.isSuccess() ? Try.ok(impl, null) : Try.fail(impl, reason(r, "insmod")); + } + + @NonNull + private List> unloadLadder() { + var log = new ArrayList>(); + rung(null, log, Only.DEFAULT, () -> { + var r = runList("rmmod", "gh_hugepage_reserve"); + return r.isSuccess() + ? Try.ok(Only.DEFAULT, null) + : Try.fail(Only.DEFAULT, reason(r, "rmmod")); + }); + return log; + } + + /** The configured target from settings.prop (pool_want, legacy pool_target). */ + private long savedWantPages() { + if (existsSticky(SETTINGS_PROP)) { + var s = parseProp(safeRead(SETTINGS_PROP)); + var v = s.getOrDefault("pool_want", s.get("pool_target")); + if (v != null) try { + return Long.parseLong(v.trim()); + } catch (NumberFormatException ignored) { + } + } + return DEFAULT_WANT_PAGES; + } + + /* ================================================================== */ + /* Shell helpers */ + /* ================================================================== */ + + /** Write a knob; returns a bare success/fail Try (single-impl call sites). */ + private static Try writeKnob(@NonNull String knob, @NonNull String value) { + return writeKnobTry(Only.DEFAULT, knob, value); + } + + /** Write a knob under the given impl marker; the Try carries the errno reason. */ + private static Try writeKnobTry( + @NonNull I impl, @NonNull String knob, @NonNull String value + ) { + var r = run("echo %s > %s/%s", value, SYSFS_PARAMS, knob); + return r.isSuccess() + ? Try.ok(impl, null) + : Try.fail(impl, reason(r, knob)); + } + + /** Persist the target to settings.prop under both keys (whichever loader reads). */ + private static Try writeSettings(long pages) { + var a = run("echo 'pool_want=%s' > %s", pages, SETTINGS_PROP); + var b = run("echo 'pool_target=%s' >> %s", pages, SETTINGS_PROP); + return (a.isSuccess() && b.isSuccess()) + ? Try.ok(Only.DEFAULT, null) + : Try.fail(Only.DEFAULT, reason(a.isSuccess() ? b : a, "settings.prop")); + } + + @NonNull + private static String reason(@NonNull RunResult r, @NonNull String what) { + var msg = r.getErrString().trim(); + if (msg.isEmpty()) msg = r.getOutString().trim(); + if (msg.isEmpty()) msg = "exit " + r.getCode(); + return what + ": " + msg; + } + + /** + * {@code test -e} that retries once on a negative. A lone root {@code test -e} + * can flake under load; retrying only the negative absorbs that transient miss + * without caching a possibly-stale positive. + */ + private static boolean existsSticky(@NonNull String path) { + return shellCheckExists(path) || shellCheckExists(path); + } + + @NonNull + private static String safeRead(@NonNull String path) { try { - raw = run( - "for f in base served_summary pool_want acquire manual_refill; do " - + "if [ \"$f\" = base ]; then t=%1$s; else t=%2$s/$f; fi; " - + "[ -e \"$t\" ] && echo 1 || echo 0; done", - SYSFS_BASE, SYSFS_PARAMS - ).getOutString(); + return shellReadFile(path); } catch (Exception e) { - raw = ""; + return ""; } - var f = raw.split("\n"); - boolean loaded = flag(f, 0); - boolean served = flag(f, 1); - return new Caps(loaded, loaded && served, flag(f, 2), flag(f, 3), flag(f, 4)); } - private static boolean flag(@NonNull String[] lines, int i) { - return i < lines.length && "1".equals(lines[i].trim()); + @NonNull + private static Map parseProp(@NonNull String raw) { + var map = new LinkedHashMap(); + for (var line : raw.split("\n")) { + var parts = line.split("=", 2); + if (parts.length == 2) map.put(parts[0].trim(), parts[1].trim()); + } + return map; + } + + private static long getLong(@NonNull Map m, @NonNull String key, long def) { + var v = m.get(key); + if (v == null) return def; + try { + return Long.parseLong(v.trim()); + } catch (NumberFormatException e) { + return def; + } } /* ================================================================== */ - /* VM name resolution (daemon vm_list) */ + /* Pool target (v6 read-only knob, cached) */ + /* ================================================================== */ + + private volatile long poolTargetCache = -1; + private volatile long poolTargetAtMs = Long.MIN_VALUE; + + /** + * v6 grow target from the read-only {@code pool_target} param, cached with a + * short TTL (static until the module is reloaded). Returns -1 if unavailable. + */ + private long poolTarget() { + long now = SystemClock.elapsedRealtime(); + if (poolTargetAtMs != Long.MIN_VALUE && now - poolTargetAtMs < STAT_TTL_MS) { + return poolTargetCache; + } + long v = -1; + try { + var t = shellReadFile(pathJoin(SYSFS_PARAMS, "pool_target")).trim(); + if (!t.isEmpty()) v = Long.parseLong(t); + } catch (Exception ignored) { + } + poolTargetCache = v; + poolTargetAtMs = now; + return v; + } + + /* ================================================================== */ + /* Usage sources (KO attribution / THP scan) */ + /* ================================================================== */ + + private Try> usageFromKo() { + if (!koAvailable()) return Try.fail(Source.KO, "KO attribution knobs absent"); + try { + // Reconcile, then read reconciled per-owner live page counts. + run("echo 1 > %s/reconcile", SYSFS_PARAMS); + var livePages = new LinkedHashMap(); + for (var ln : safeRead(pathJoin(SYSFS_PARAMS, "served_summary")).split("\n")) { + ln = ln.trim(); + if (!ln.startsWith("owner ")) continue; + int pid = -1; + long pages = 0; + for (var tok : ln.split("\\s+")) { + if (tok.startsWith("pid=")) pid = parseIntSafe(tok.substring(4)); + else if (tok.startsWith("pages=")) pages = parseLongSafe(tok.substring(6)); + } + if (pid > 0) livePages.put(pid, pages); + } + var owners = parseOwners(safeRead(SYSFS_OWNERS)); + var pids = new ArrayList(); + for (var o : owners) pids.add(o.pid); + var procInfo = readProcInfo(pids); + + var out = new ArrayList(); + for (var o : owners) { + var info = procInfo.get(o.pid); + char state = info != null ? info.state : '?'; + boolean alive = info != null && info.alive; + // Occupancy = reconciled served count (not the /proc scan; /proc is + // only used for run state + liveness). + Long served = livePages.get(o.pid); + out.add(new UsageEntry(o.pid, o.comm, served != null ? served : 0L, state, alive)); + } + out.sort((a, b) -> Long.compare(b.pages, a.pages)); + return Try.ok(Source.KO, out); + } catch (Exception e) { + return Try.fail(Source.KO, "KO read: " + e.getMessage()); + } + } + + private Try> usageFromScan() { + // Running VMs (pid -> name) come from the daemon; scan only those, not all + // of /proc (system-wide THP is noise unrelated to the pool). + var vmMap = vmNames(false); + if (vmMap.isEmpty()) return Try.fail(Source.SCAN, "no running VM pids"); + var procInfo = readProcInfo(vmMap.keySet()); + + var out = new ArrayList(); + for (var e : vmMap.entrySet()) { + int pid = e.getKey(); + var info = procInfo.get(pid); + char state = info != null ? info.state : '?'; + long thpKb = info != null ? info.thpKb : -1; + long pages = thpKb > 0 ? thpKb / KB_PER_PAGE : 0; + boolean alive = info != null && info.alive; + out.add(new UsageEntry(pid, e.getValue(), pages, state, alive)); + } + out.sort((a, b) -> Long.compare(b.pages, a.pages)); + return Try.ok(Source.SCAN, out); + } + + /* ---- vm_owners parsing ("pid= served= comm=") ---- */ + + private static final class Owner { + final int pid; + @NonNull final String comm; + + Owner(int pid, @NonNull String comm) { + this.pid = pid; + this.comm = comm; + } + } + + @NonNull + private static List parseOwners(@NonNull String raw) { + var list = new ArrayList(); + for (var line : raw.split("\n")) { + line = line.trim(); + if (line.isEmpty()) continue; + int pid = -1; + String comm = "?"; + int commIdx = line.indexOf("comm="); + if (commIdx >= 0) comm = line.substring(commIdx + 5).trim(); + for (var tok : line.split("\\s+")) { + if (tok.startsWith("pid=")) pid = parseIntSafe(tok.substring(4)); + } + if (pid > 0) list.add(new Owner(pid, comm)); + } + return list; + } + + private static int parseIntSafe(@NonNull String s) { + try { + return Integer.parseInt(s.trim()); + } catch (NumberFormatException e) { + return -1; + } + } + + private static long parseLongSafe(@NonNull String s) { + try { + return Long.parseLong(s.trim()); + } catch (NumberFormatException e) { + return 0; + } + } + + /* ================================================================== */ + /* VM name resolution (daemon vm_list), cached */ /* ================================================================== */ @NonNull @@ -153,10 +702,8 @@ private static boolean flag(@NonNull String[] lines, int i) { /** * Best-effort pid→VM-name map from the daemon, cached for * {@link #VM_TTL_MS}. Within the TTL the cached map is returned without - * contacting the daemon, so the refresh loop no longer blocks up to 2s every - * tick. On a fetch that times out the last known map is kept (labels stay - * stable instead of blanking to raw pids). Pass {@code force = true} to - * bypass the cache (e.g. right after killing a process). + * contacting the daemon. On a timeout the last known map is kept (labels stay + * stable instead of blanking to raw pids). Pass {@code force} to bypass the cache. */ @NonNull Map vmNames(boolean force) { @@ -171,16 +718,6 @@ Map vmNames(boolean force) { return vmNames; } - /** - * One blocking daemon {@code vm_list} round-trip (bounded to - * {@link #DAEMON_TIMEOUT_S}). Returns {@code null} on timeout/error so the - * caller can distinguish "no answer" from a genuine empty VM list. - * - *

The map is published through an {@link AtomicReference} set inside the - * callback, so the caller either sees a fully-built map or {@code null} — - * never a half-populated one, even when the response lands right at the - * timeout boundary. - */ @Nullable private Map fetchVmMap() { var result = new AtomicReference>(null); @@ -291,86 +828,4 @@ Map readProcInfo(@NonNull Collection pids) { if (curPid > 0) map.put(curPid, new ProcInfo(state, thp, alive)); return map; } - - /** - * Per-VM THP occupancy in pages for the given pids, as {pid, pages} - * rows, dropping any VM with under one full huge page. Used by the status - * screen's scan-fallback bar; a thin wrapper over {@link #readProcInfo}. - */ - @NonNull - List scanThpPages(@NonNull Collection pids) { - var result = new ArrayList(); - var procInfo = readProcInfo(pids); - for (var pid : pids) { - var info = procInfo.get(pid); - if (info == null || info.thpKb <= 0) continue; - long pages = info.thpKb / KB_PER_PAGE; - if (pages > 0) result.add(new long[]{pid, pages}); - } - return result; - } - - /* ================================================================== */ - /* Pool stats */ - /* ================================================================== */ - - private volatile long poolTargetCache = -1; - private volatile long poolTargetAtMs = Long.MIN_VALUE; - - /** - * v6 grow target from the read-only {@code pool_target} param, cached with a - * short TTL (it is static until the module is reloaded / the app restarts). - * Returns -1 if unavailable. This is what stops the v6 refresh loop from - * re-{@code cat}-ing the same static file every tick. - */ - private long poolTarget() { - long now = SystemClock.elapsedRealtime(); - if (poolTargetAtMs != Long.MIN_VALUE && now - poolTargetAtMs < CAPS_TTL_MS) { - return poolTargetCache; - } - long v = -1; - try { - var t = shellReadFile(pathJoin(SYSFS_PARAMS, "pool_target")).trim(); - if (!t.isEmpty()) v = Long.parseLong(t); - } catch (Exception ignored) { - } - poolTargetCache = v; - poolTargetAtMs = now; - return v; - } - - /** - * Read the pool counters from {@code refill_stat}. - * Returns {@code {total, avail, want, acquiring, served}} in pages, or - * {@code null} if the module isn't loaded / stats are unreadable. {@code want} - * falls back to v6's cached {@code pool_target}, then to capacity. - */ - @Nullable - long[] readPoolPages() { - try { - if (!shellCheckExists(SYSFS_BASE)) return null; - var raw = shellReadFile(pathJoin(SYSFS_PARAMS, "refill_stat")); - long total = -1, avail = 0, want = -1, acquiring = 0, served = 0; - for (var line : raw.split("\n")) { - line = line.trim(); - if (line.startsWith("pool_total=")) { - total = Long.parseLong(line.substring(11).trim()); - } else if (line.startsWith("pool_avail=")) { - avail = Long.parseLong(line.substring(11).trim()); - } else if (line.startsWith("pool_want=")) { - want = Long.parseLong(line.substring(10).trim()); - } else if (line.startsWith("acquire_active=")) { - acquiring = Long.parseLong(line.substring(15).trim()); - } else if (line.startsWith("served=")) { - served = Long.parseLong(line.substring(7).trim()); - } - } - if (total < 0) return null; - if (want < 0) want = poolTarget(); // v6: separate read-only knob - if (want < 0) want = total; - return new long[]{total, avail, want, acquiring, served}; - } catch (Exception e) { - return null; - } - } } diff --git a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcessActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcessActivity.java index 1f1bc9e00..1bdce4994 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcessActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcessActivity.java @@ -9,8 +9,6 @@ import static cn.classfun.droidvm.lib.utils.ProcessUtils.SIGKILL; import static cn.classfun.droidvm.lib.utils.ProcessUtils.SIGTERM; import static cn.classfun.droidvm.lib.utils.ProcessUtils.shellKillProcess; -import static cn.classfun.droidvm.lib.utils.RunUtils.run; -import static cn.classfun.droidvm.lib.utils.StringUtils.pathJoin; import static cn.classfun.droidvm.lib.utils.ThreadUtils.runOnPool; import android.os.Bundle; @@ -34,9 +32,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; import cn.classfun.droidvm.R; import cn.classfun.droidvm.lib.size.SizeUtils; @@ -50,9 +46,6 @@ public final class HugePageProcessActivity extends AppCompatActivity implements HugePageProcessAdapter.Listener { private static final String TAG = "HugePageProcessActivity"; - private static final String SYSFS_BASE = "/sys/module/gh_hugepage_reserve"; - private static final String SYSFS_PARAMS = pathJoin(SYSFS_BASE, "parameters"); - private static final String SYSFS_OWNERS = pathJoin(SYSFS_PARAMS, "vm_owners"); private static final long REFRESH_INTERVAL_MS = 2000; /** Acquire progress poll: ~500 ms ticks, capped so a wedged run can't poll forever. */ private static final long ACQUIRE_POLL_MS = 500; @@ -73,6 +66,7 @@ public final class HugePageProcessActivity extends AppCompatActivity private boolean koWasPresent = true; private boolean firstRefresh = true; private volatile boolean acquireWatching = false; + private volatile int acquireWatchMode = -1; // mode we optimistically started private MaterialToolbar toolbar; private RecyclerView recyclerView; private TextView tvEmpty; @@ -195,55 +189,19 @@ private void refresh() { } runOnPool(() -> { boolean dark = HugePageColor.isDark(this); - // KO attribution availability comes from the cached capability probe - // (re-probed at most every few seconds, with a retry on a negative), - // so a single flaky root `test -e` can't flap the whole view between - // KO and scan every tick. Absent -> force scan, disable the KO entry. - // A present-but-empty pool (no VM) keeps KO available ("no VM"). - boolean koNow = model.caps(false).koAttribution; - boolean useScan = scanMode || !koNow; - - List list = new ArrayList<>(); - String emptyText; - if (useScan) { - // Scan mode: system-wide THP (Anon + Shmem), no module needed. - try { - list = scanProc(dark); - } catch (Exception e) { - Log.w(TAG, "Scan failed", e); - } - emptyText = getString(R.string.hugepage_proc_scan_empty); - } else { - // Reconcile the served-page table on every query, then read the - // reconciled per-owner live page counts. - var livePages = new HashMap(); - try { - run("echo 1 > %s/reconcile", SYSFS_PARAMS); - var sum = shellReadFile(pathJoin(SYSFS_PARAMS, "served_summary")); - for (var ln : sum.split("\n")) { - ln = ln.trim(); - if (!ln.startsWith("owner ")) continue; - int pid = -1; - long pages = 0; - for (var tok : ln.split("\\s+")) { - if (tok.startsWith("pid=")) pid = Integer.parseInt(tok.substring(4)); - else if (tok.startsWith("pages=")) pages = Long.parseLong(tok.substring(6)); - } - if (pid > 0) livePages.put(pid, pages); - } - } catch (Exception e) { - Log.w(TAG, "reconcile/served_summary unavailable (old .ko?)", e); - } - try { - var owners = parseOwners(shellReadFile(SYSFS_OWNERS)); - if (!owners.isEmpty()) list = buildFromOwners(owners, dark, livePages); - } catch (Exception e) { - Log.w(TAG, "Failed to collect hugepage owners", e); - } - // Reached only when the KO API is present (else we auto-switched - // to scan above), so an empty list genuinely means "no VM". - emptyText = getString(R.string.hugepage_proc_owner_empty); - } + // Pull the usage list through the degradation ladder: honour the user's + // scan preference by pinning SCAN, else auto (KO, degrading to a THP scan + // when the module's attribution knobs are absent). The winning impl says + // what's actually shown; koAvailable() says whether KO *could* be used, + // to keep the mode menu + toast meaningful even while showing a scan. + var usage = model.usage(scanMode ? HugePageModel.Source.SCAN : null); + boolean koNow = model.koAvailable(); + boolean useScan = usage.source != HugePageModel.Source.KO; + + List list = buildFromEntries(usage.entries, dark); + String emptyText = getString(useScan + ? R.string.hugepage_proc_scan_empty + : R.string.hugepage_proc_owner_empty); // Three-value model: hold (pages in pool) + traced (sum of all // applied/served processes) + waiting-to-acquire = want. @@ -255,16 +213,11 @@ private void refresh() { // orphaned (served-but-owner-gone) pages alike. long tracedKb = 0; for (var r : list) if (r.thpKb > 0) tracedKb += r.thpKb; - long holdKb, wantKb; - var pool = model.readPoolPages(); - boolean kernelAcquiring = pool != null && pool.length >= 4 && pool[3] != 0; - if (pool != null) { - holdKb = pool[1] * 2048; // avail - wantKb = pool[2] * 2048; // want - } else { - holdKb = 0; - wantKb = tracedKb; - } + var snap = model.state(); + boolean kernelAcquiring = snap.acquiring; + int kernelMode = snap.acquireMode; + long holdKb = snap.loaded ? snap.free * 2048 : 0; // avail + long wantKb = snap.loaded ? snap.targetIdeal * 2048 : tracedKb; // want long deficitKb = wantKb - holdKb - tracedKb; if (deficitKb > 0) { list.add(new HugePageProcess( @@ -279,6 +232,7 @@ private void refresh() { var fHold = holdKb; var fWant = wantKb; var fAcquiring = kernelAcquiring; + var fMode = kernelMode; runOnUiThread(() -> { koPresent = fKoNow; // Toast only on the present->absent edge while the user wants KO, @@ -292,114 +246,33 @@ private void refresh() { // (right after inflateMenu, before the menu is laid out) doesn't // stick, leaving the radio showing the wrong mode. updateModeChecked(); - adapter.setAcquiring(fAcquiring || acquireWatching); + int uiMode = fAcquiring ? fMode + : (acquireWatching ? acquireWatchMode : -1); + adapter.setAcquireState(fAcquiring || acquireWatching, uiMode); showResult(finalList, fTraced, fHold, fWant, finalEmpty); }); }); } + /** Map ladder usage entries to display rows (color + VM-name enrichment). */ @NonNull - private List buildFromOwners( - @NonNull List owners, boolean dark, - @NonNull Map livePages + private List buildFromEntries( + @NonNull List entries, boolean dark ) { - var pids = new ArrayList(); - for (var o : owners) pids.add(o.pid); - var procInfo = model.readProcInfo(pids); - - // Best-effort map pid -> VM name (only running VMs have a live pid). - var vmMap = model.vmNames(false); - - var result = new ArrayList(); - for (var o : owners) { - var info = procInfo.get(o.pid); - char state = info != null ? info.state : '?'; - boolean alive = info != null && info.alive; - // Occupancy = traced (reconciled served-page count for this owner), - // not the /proc THP scan. /proc is only used for state + liveness. - Long served = livePages.get(o.pid); - long tracedKb = (served != null ? served : 0L) * 2048; - result.add(new HugePageProcess( - o.pid, o.comm, -1, tracedKb, state, - vmMap.get(o.pid), alive, HugePageColor.forPid(o.pid, dark), - false, false)); - } - result.sort((a, b) -> Long.compare(b.thpKb, a.thpKb)); - return result; - } - - /** - * VM-only "scan": list the daemon's running VM processes and their actual - * THP occupancy (AnonHugePages + ShmemPmdMapped) from smaps. We deliberately - * do NOT scan all of /proc - system-wide THP (launchers, etc.) is noise that - * has nothing to do with the reserve pool. Works without the kernel module - * (ground-truth occupancy straight from each VM's smaps). - */ - @NonNull - private List scanProc(boolean dark) { - // Running VMs (pid -> name) come from the daemon. + // Best-effort pid -> VM name (running VMs only); for KO rows this labels + // the process, for scan rows it echoes the name the entry already carries. var vmMap = model.vmNames(false); - if (vmMap.isEmpty()) return new ArrayList<>(); - - var procInfo = model.readProcInfo(vmMap.keySet()); - var result = new ArrayList(); - for (var e : vmMap.entrySet()) { - int pid = e.getKey(); - var info = procInfo.get(pid); - char state = info != null ? info.state : '?'; - long thp = info != null ? info.thpKb : -1; - boolean alive = info != null && info.alive; + for (var e : entries) { result.add(new HugePageProcess( - pid, e.getValue(), -1, thp, state, e.getValue(), alive, - HugePageColor.forPid(pid, dark), false, false)); + e.pid, e.comm, -1, e.pages * 2048, e.state, + vmMap.getOrDefault(e.pid, e.comm), e.alive, + HugePageColor.forPid(e.pid, dark), false, false)); } result.sort((a, b) -> Long.compare(b.thpKb, a.thpKb)); return result; } - private static final class Owner { - final int pid; - final String comm; - final long served; - - Owner(int pid, String comm, long served) { - this.pid = pid; - this.comm = comm; - this.served = served; - } - } - - @NonNull - private List parseOwners(@NonNull String raw) { - var list = new ArrayList(); - for (var line : raw.split("\n")) { - line = line.trim(); - if (line.isEmpty()) continue; - // Format: pid= served= comm= - int pid = -1; - long served = 0; - String comm = "?"; - int commIdx = line.indexOf("comm="); - if (commIdx >= 0) comm = line.substring(commIdx + 5).trim(); - for (var tok : line.split("\\s+")) { - if (tok.startsWith("pid=")) { - try { - pid = Integer.parseInt(tok.substring(4)); - } catch (NumberFormatException ignored) { - } - } else if (tok.startsWith("served=")) { - try { - served = Long.parseLong(tok.substring(7)); - } catch (NumberFormatException ignored) { - } - } - } - if (pid > 0) list.add(new Owner(pid, comm, served)); - } - return list; - } - private void showResult( @NonNull List list, long usedKb, long availKb, long wantKb, @NonNull String emptyText @@ -491,64 +364,62 @@ private void doKill(@NonNull HugePageProcess proc, int signal) { } @Override - public void onAcquire() { + public void onAcquire(int mode) { if (acquireWatching) return; // already polling a run Toast.makeText(this, R.string.hugepage_proc_acquire_running, LENGTH_SHORT).show(); acquireWatching = true; - adapter.setAcquiring(true); // immediate spinner feedback + acquireWatchMode = mode; // remember which we just started + adapter.setAcquireState(true, mode); // immediate spinner feedback // Fire-and-poll on a dedicated thread so the shared pool executor (and // thus the periodic refresh that animates the climbing numbers) is never // blocked. The kernel 'acquire' write returns immediately; the worker // migrates in the background and we watch acquire_active for completion. new Thread(() -> { - var caps = model.caps(false); - boolean v7 = caps.hasAcquire; - boolean triggered = false; - try { - if (v7) { - triggered = run("echo 1 > %s/acquire", SYSFS_PARAMS).isSuccess(); - } else if (caps.hasManualRefill) { - triggered = run("echo 1 > %s/manual_refill", SYSFS_PARAMS).isSuccess(); - } - } catch (Exception e) { - Log.w(TAG, "acquire trigger failed", e); - } + // Fill the pool via the acquire ladder: the migrating 'acquire' knob, + // degrading to 'manual_refill' on any failure (e.g. -ENOSYS on kernels + // that can't migrate). The trace's last entry says what actually ran. + var res = model.acquire(mode); + boolean triggered = res.ok(); + // Only a migrating acquire mode exposes acquire_active to poll; the + // manual_refill fallback is fire-and-forget with no progress to watch. + boolean migrating = triggered && !"manual_refill".equals(res.impl); + final boolean degraded = res.degraded; // ran below the requested mode + final String actualImpl = res.impl != null ? res.impl : ""; if (!triggered) { acquireWatching = false; runOnUiThread(() -> Toast.makeText(this, R.string.hugepage_refill_failed, LENGTH_SHORT).show()); return; } - if (!v7) { - // v6 (manual_refill): an async refill with no acquire_active to - // poll and no served=/pool_want to report a precise got/want. - // Just trigger it and let the periodic refresh animate the bar. + if (!migrating) { + // Fell all the way to manual_refill: async, no acquire_active/served + // to poll. Name the fallback (it is itself the degradation). acquireWatching = false; runOnUiThread(() -> { - Toast.makeText(this, R.string.hugepage_proc_acquire_done, - LENGTH_SHORT).show(); + Toast.makeText(this, + getString(R.string.hugepage_acquire_degraded, actualImpl), + LENGTH_LONG).show(); refresh(); }); return; } - // v7: poll until the worker clears acquire_active. Each tick nudges + // Migrating: poll until the worker clears acquire_active. Each tick nudges // the UI to re-read refill_stat so the count visibly climbs. - long[] pages = null; - for (int i = 0; i < ACQUIRE_POLL_MAX; i++) { + boolean running = true; + for (int i = 0; i < ACQUIRE_POLL_MAX && running; i++) { try { Thread.sleep(ACQUIRE_POLL_MS); } catch (InterruptedException ignored) {} - pages = model.readPoolPages(); // {total, avail, want, acquiring, served} - runOnUiThread(this::refresh); // animate climbing numbers - if (pages == null || pages.length < 5 || pages[3] == 0) break; + runOnUiThread(this::refresh); // animate climbing numbers (reads full state) + running = model.acquiring(); // cheap acquire_active-only probe } - var finalPages = pages; + var finalSnap = model.state(); // one full read for the final got/want acquireWatching = false; runOnUiThread(() -> { - if (finalPages != null && finalPages.length >= 5) { + if (finalSnap != null && finalSnap.loaded) { // Achieved = owned + traced (pool_avail + served), NOT // pool_total: after a shrink that left served pages out, // pool_total under-counts the pages VMs already hold. - long got = finalPages[1] + finalPages[4]; - long want = finalPages[2] >= 0 ? finalPages[2] : got; + long got = finalSnap.free + finalSnap.lent; + long want = finalSnap.targetIdeal; String msg = got >= want ? getString(R.string.hugepage_proc_acquire_full, fmtPages(want)) @@ -559,20 +430,26 @@ public void onAcquire() { Toast.makeText(this, R.string.hugepage_proc_acquire_done, LENGTH_SHORT).show(); } + // Requested a higher mode than could run (e.g. v3 -> v2) -> name it. + if (degraded) Toast.makeText(this, + getString(R.string.hugepage_acquire_degraded, actualImpl), + LENGTH_LONG).show(); refresh(); }); }, "hugepage-acquire").start(); } + /** Long-press an acquire button: explain the mode, with a Run/Cancel choice. */ + @Override + public void onAcquireInfo(int mode) { + HugePageActivity.showAcquireInfo(this, mode, () -> onAcquire(mode)); + } + /** Tapping the acquire spinner stops the run; the poll loop then finishes. */ @Override public void onStopAcquire() { runOnPool(() -> { - try { - run("echo 0 > %s/acquire", SYSFS_PARAMS); - } catch (Exception e) { - Log.w(TAG, "stop acquire failed", e); - } + model.stopAcquire(); runOnUiThread(this::refresh); }); } diff --git a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcessAdapter.java b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcessAdapter.java index 88aee65ac..1f41f3fa9 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcessAdapter.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcessAdapter.java @@ -32,7 +32,12 @@ public interface Listener { void onShowStack(@NonNull HugePageProcess proc); - void onAcquire(); + /** @param mode acquire algorithm: 2 = migrate + system reclaim, + * 3 = migrate + per-block evict to zram. */ + void onAcquire(int mode); + + /** Long-press: show the what-does-this-mode-do dialog (with a Run choice). */ + void onAcquireInfo(int mode); void onStopAcquire(); } @@ -41,16 +46,23 @@ public interface Listener { private final List items = new ArrayList<>(); private final Listener listener; private boolean acquiring = false; + private int acquireMode = -1; // running v (1/2/3); -1 = unknown (old module) public HugePageProcessAdapter(@NonNull Listener listener) { this.listener = listener; setHasStableIds(true); } - /** Toggle the spinner on the Acquire row while a run is in flight. */ - public void setAcquiring(boolean acquiring) { - if (this.acquiring == acquiring) return; + /** + * Reflect the acquire state on the row: idle = three buttons, running with a + * known mode = that slot spins + the other two greyed out, running with an + * unknown mode (-1, old module) = all three spin. + */ + public void setAcquireState(boolean acquiring, int mode) { + if (!acquiring) mode = -1; + if (this.acquiring == acquiring && this.acquireMode == mode) return; this.acquiring = acquiring; + this.acquireMode = mode; notifyDataSetChanged(); } @@ -104,29 +116,19 @@ public void onBindViewHolder(@NonNull Holder holder, int position) { holder.btnStack.setVisibility(GONE); holder.btnStack.setOnClickListener(null); holder.itemView.setAlpha(1f); + holder.btnKill.setVisibility(GONE); + holder.btnKill.setOnClickListener(null); if (p.acquire) { - // While a run is in flight show the spinner in place of the - // button - same look as before, but tapping it stops the acquire. - holder.progressAcquire.setVisibility(acquiring ? VISIBLE : GONE); - holder.progressAcquire.setOnClickListener( - acquiring ? v -> listener.onStopAcquire() : null); - holder.btnKill.setVisibility(acquiring ? GONE : VISIBLE); - holder.btnKill.setImageResource(R.drawable.ic_download); - holder.btnKill.setColorFilter(MaterialColors.getColor( - holder.itemView, androidx.appcompat.R.attr.colorPrimary)); - holder.btnKill.setContentDescription( - ctx.getString(R.string.hugepage_proc_acquire)); - holder.btnKill.setOnClickListener(v -> listener.onAcquire()); + bindAcquireSlot(holder.btnAcquireV1, holder.progressAcquireV1, 1); + bindAcquireSlot(holder.btnAcquireV2, holder.progressAcquireV2, 2); + bindAcquireSlot(holder.btnAcquireV3, holder.progressAcquireV3, 3); } else { - holder.progressAcquire.setVisibility(GONE); - holder.progressAcquire.setOnClickListener(null); - holder.btnKill.setVisibility(GONE); - holder.btnKill.setOnClickListener(null); + hideAcquireSlots(holder); } return; } - holder.progressAcquire.setVisibility(GONE); + hideAcquireSlots(holder); holder.btnKill.setVisibility(VISIBLE); // Restore the kill affordance (views are recycled from acquire rows). holder.btnKill.setImageResource(R.drawable.ic_close); @@ -173,6 +175,34 @@ public void onBindViewHolder(@NonNull Holder holder, int position) { holder.btnKill.setOnClickListener(dead ? null : v -> listener.onKill(p)); } + /** One acquire slot: spins iff running AND (mode unknown or this mode); when a + * run is in flight the non-running slots are visible-but-disabled buttons. */ + private void bindAcquireSlot(View btn, View spinner, int mode) { + boolean spin = acquiring && (acquireMode < 0 || acquireMode == mode); + btn.setVisibility(spin ? GONE : VISIBLE); + // Left enabled so long-press always opens the info dialog; short-press is + // gated on !acquiring and the icon greys via alpha while a run is in flight. + btn.setAlpha(acquiring ? 0.38f : 1f); + btn.setOnClickListener(v -> { if (!acquiring) listener.onAcquire(mode); }); + btn.setOnLongClickListener(v -> { listener.onAcquireInfo(mode); return true; }); + spinner.setVisibility(spin ? VISIBLE : GONE); + spinner.setOnClickListener(spin ? v -> listener.onStopAcquire() : null); + } + + /** Hide all six acquire views and reset the buttons' enabled state (recycled). */ + private static void hideAcquireSlots(Holder h) { + View[] all = {h.btnAcquireV1, h.btnAcquireV2, h.btnAcquireV3, + h.progressAcquireV1, h.progressAcquireV2, h.progressAcquireV3}; + for (View v : all) { + v.setVisibility(GONE); + v.setOnClickListener(null); + } + for (View b : new View[]{h.btnAcquireV1, h.btnAcquireV2, h.btnAcquireV3}) { + b.setOnLongClickListener(null); + b.setAlpha(1f); + } + } + static final class Holder extends RecyclerView.ViewHolder { final ImageView icon; final TextView title; @@ -181,7 +211,12 @@ static final class Holder extends RecyclerView.ViewHolder { final TextView stateView; final ImageButton btnStack; final ImageButton btnKill; - final View progressAcquire; + final View btnAcquireV1; + final View btnAcquireV2; + final View btnAcquireV3; + final View progressAcquireV1; + final View progressAcquireV2; + final View progressAcquireV3; Holder(@NonNull View v) { super(v); @@ -192,7 +227,12 @@ static final class Holder extends RecyclerView.ViewHolder { stateView = v.findViewById(R.id.tv_proc_state); btnStack = v.findViewById(R.id.btn_proc_stack); btnKill = v.findViewById(R.id.btn_proc_kill); - progressAcquire = v.findViewById(R.id.progress_proc_acquire); + btnAcquireV1 = v.findViewById(R.id.btn_acquire_v1); + btnAcquireV2 = v.findViewById(R.id.btn_acquire_v2); + btnAcquireV3 = v.findViewById(R.id.btn_acquire_v3); + progressAcquireV1 = v.findViewById(R.id.progress_acquire_v1); + progressAcquireV2 = v.findViewById(R.id.progress_acquire_v2); + progressAcquireV3 = v.findViewById(R.id.progress_acquire_v3); } } } diff --git a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/Try.java b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/Try.java new file mode 100644 index 000000000..84df9409a --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/Try.java @@ -0,0 +1,48 @@ +package cn.classfun.droidvm.ui.hugepage; + +import androidx.annotation.Nullable; + +/** + * One implementation's attempt within a degradation ladder: which implementation + * ran, plus its return value on success or its failure reason otherwise. The pair + * {@code (value | error, impl)} is what a {@link HugePageModel} feature call + * accumulates into an ordered {@code List>}; a success stops the ladder, + * so the caller judges the overall outcome from the last element: + *

    + *
  • last {@link #ok()} and alone — success via the primary implementation;
  • + *
  • last {@link #ok()} after earlier fails — success, degraded to a fallback;
  • + *
  • last not {@link #ok()} — failure, and every rung's reason is in the list.
  • + *
+ * + * @param the feature's implementation enum (which way it was done) + * @param the value produced on success ({@link Void} for pure actions) + */ +final class Try { + /** The implementation this attempt used. */ + final I impl; + /** The value produced on success; {@code null} on failure (or for {@link Void}). */ + @Nullable + final V value; + /** The failure reason; {@code null} on success. */ + @Nullable + final String error; + + private Try(I impl, @Nullable V value, @Nullable String error) { + this.impl = impl; + this.value = value; + this.error = error; + } + + static Try ok(I impl, @Nullable V value) { + return new Try<>(impl, value, null); + } + + static Try fail(I impl, String error) { + return new Try<>(impl, null, error); + } + + /** True if this implementation succeeded (a non-null error means failure). */ + boolean ok() { + return error == null; + } +} diff --git a/app/src/main/res/layout/acquire_buttons.xml b/app/src/main/res/layout/acquire_buttons.xml new file mode 100644 index 000000000..fb26d474c --- /dev/null +++ b/app/src/main/res/layout/acquire_buttons.xml @@ -0,0 +1,197 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_hugepage.xml b/app/src/main/res/layout/activity_hugepage.xml index 7f2510171..44b571294 100644 --- a/app/src/main/res/layout/activity_hugepage.xml +++ b/app/src/main/res/layout/activity_hugepage.xml @@ -121,7 +121,7 @@ android:icon="@drawable/ic_resize" android:inputType="number" app:ti_max="16GiB" - app:ti_min="256MiB" + app:ti_min="0MiB" app:ti_mode="size" app:ti_precision="2MiB" /> @@ -260,6 +260,37 @@ android:text="@string/hugepage_view_processes" app:icon="@drawable/ic_file_tree" /> + + + + + + + + + + + + + - - - - - - - - diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 0fed84dc5..ca74410a5 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -964,6 +964,18 @@ 池大小已保存并应用 获取 获取完成 + 获取大页 + 执行 + 已降级,实际使用 %1$s + v1 + v2 + v3 + 获取 v1(alloc_contig_pages 扫描) + 获取 v2(迁移+系统回收) + 获取 v3(迁移+逐块挤到 zram) + 用 alloc_contig_pages 收集大页 + 用 try_to_free_mem_cgroup_pages 把其他程序挤到 swap/zram\n再用 alloc_contig_range 收集大页 + 用 folio_isolate_lru + reclaim_pages 精准把页面挤到 swap/zram\n再用 alloc_contig_range 收集大页 停止获取 获取完成:已达成 %1$s 获取结束:达成 %1$s / 目标 %2$s(系统目前无法再凑出更多) diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index ca90d6224..63c8f8c83 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -922,11 +922,23 @@ 獲取 獲取大頁中… 獲取完成 + v1 + v2 + v3 + 獲取 v1(alloc_contig_pages 掃描) + 獲取 v2(遷移+系統回收) + 獲取 v3(遷移+逐塊擠到 zram) + 已降級,實際使用 %1$s + 執行 + 用 alloc_contig_pages 蒐集大頁 + 用 try_to_free_mem_cgroup_pages 把其他程式擠到 swap/zram\n再用 alloc_contig_range 蒐集大頁 + 用 folio_isolate_lru + reclaim_pages 精準把頁面擠到 swap/zram\n再用 alloc_contig_range 蒐集大頁 停止獲取 獲取完成:已達成 %1$s 獲取結束:達成 %1$s / 目標 %2$s(系統目前無法再湊出更多) 模組不足,嘗試 userspace 協助… 觸發獲取失敗 + 獲取大頁 池大小 池大小已儲存(重啟後生效) 池大小已儲存並套用 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7692a340f..1ab0997f6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -962,12 +962,24 @@ Failed to trigger acquire Save Acquire + v1 + v2 + v3 + Acquire v1 (alloc_contig_pages scan) + Acquire v2 (migrate + system reclaim) + Acquire v3 (migrate + per-block evict to zram) + Run + Uses alloc_contig_pages to collect huge pages + Uses try_to_free_mem_cgroup_pages to push other apps into swap/zram\nthen alloc_contig_range to collect huge pages + Uses folio_isolate_lru + reclaim_pages to precisely push pages into swap/zram\nthen alloc_contig_range to collect huge pages Acquiring huge pages… Acquire done + Downgraded — ran %1$s instead Stop acquiring Acquire complete: reached %1$s Acquire stopped: got %1$s of %2$s (system can\'t migrate more right now) Module short; trying userspace assist… + Acquire huge pages Pool Size Pool size saved (effective after reboot) Pool size saved & applied From 35fc525cc1e33f521b74a7dcbfefb184f42d3819 Mon Sep 17 00:00:00 2001 From: HuJK Date: Sun, 5 Jul 2026 10:22:43 +0800 Subject: [PATCH 081/182] prebuilts: bump submodule to 2c31cec Point the prebuilts submodule at 2c31cecdedb5110e8861b8b698ba5b538725cfb7 (same DroidVM-Prebuilt-Root 4016dba, rebuilt). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01TaHcvnHaZk8qqpKiS25Hbk --- app/src/main/assets/prebuilts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/assets/prebuilts b/app/src/main/assets/prebuilts index fc972f187..2c31cecde 160000 --- a/app/src/main/assets/prebuilts +++ b/app/src/main/assets/prebuilts @@ -1 +1 @@ -Subproject commit fc972f1871a2accb31fc0d5f2c08cc8a37d8dd8a +Subproject commit 2c31cecdedb5110e8861b8b698ba5b538725cfb7 From fc0359bb8b46a341abf463783b7d8c44d2498f65 Mon Sep 17 00:00:00 2001 From: HuJK Date: Sun, 5 Jul 2026 10:22:43 +0800 Subject: [PATCH 082/182] (fix) fullscreen mode doesn't work correctly --- .../display/vnc/display/VMVncDisplayActivity.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/display/VMVncDisplayActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/display/VMVncDisplayActivity.java index 1961f7896..e177de753 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/display/VMVncDisplayActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/display/VMVncDisplayActivity.java @@ -346,6 +346,15 @@ private boolean onMouseTouch(View v, MotionEvent event) { return true; case MotionEvent.ACTION_POINTER_DOWN: gestureMaxPointers = max(gestureMaxPointers, pc); + // Re-anchor the tap reference to the multi-finger midpoint and + // restart the tap window. The ACTION_DOWN anchor was a single + // finger position, so the two-finger midpoint sits ~half a + // finger-spread away and would instantly trip gestureMoved, + // making a still two-finger tap (right click) impossible. + gestureStartMidX = midX(event); + gestureStartMidY = midY(event); + gestureStartTime = System.currentTimeMillis(); + gestureMoved = false; if (pc == 2) initTwoFinger(event); if (pc >= 3) lastScrollMidY = midY(event); return true; @@ -626,7 +635,9 @@ private void toggleFullscreen() { var controller = getWindow().getInsetsController(); if (controller == null) return; if (isFullscreen) { - hideBars(); + mainHandler.removeCallbacks(this::hideBars); + toolbar.setVisibility(GONE); + statusBar.setVisibility(GONE); extraKeysPanel.setVisibility(GONE); controller.hide(WindowInsets.Type.systemBars()); controller.setSystemBarsBehavior(BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); From a00a3d56652a573adb889f429f43701449b131d4 Mon Sep 17 00:00:00 2001 From: HuJK Date: Sun, 5 Jul 2026 10:39:09 +0800 Subject: [PATCH 083/182] vm: route list auto-open-after-start through the same console picker as VM info Starting a VM from the list auto-opened VMConsoleActivity (UART) unconditionally (MainVMFragment.onVMStateChanged hard-coded it), while the VM-info console button (ConsoleButton.openDefaultConsole) is display-aware: native display, else VNC, else the serial console. So a VNC/native VM started from the list wrongly popped the UART console instead of its display. Extract that routing into a shared VmConsoleRouter.openDefault(); the info button and the list auto-open now both call it, so they agree. ConsoleButton's chooser open-helpers delegate to the router too (single source of truth). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01TaHcvnHaZk8qqpKiS25Hbk --- .../droidvm/ui/main/vm/MainVMFragment.java | 9 +- .../ui/vm/console/VmConsoleRouter.java | 104 ++++++++++++++++++ .../droidvm/ui/vm/info/ConsoleButton.java | 69 ++---------- 3 files changed, 119 insertions(+), 63 deletions(-) create mode 100644 app/src/main/java/cn/classfun/droidvm/ui/vm/console/VmConsoleRouter.java diff --git a/app/src/main/java/cn/classfun/droidvm/ui/main/vm/MainVMFragment.java b/app/src/main/java/cn/classfun/droidvm/ui/main/vm/MainVMFragment.java index 53c084c27..442b18a93 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/main/vm/MainVMFragment.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/main/vm/MainVMFragment.java @@ -28,7 +28,7 @@ import cn.classfun.droidvm.lib.store.vm.VMStore; import cn.classfun.droidvm.ui.main.base.stateful.MainStatefulFragment; import cn.classfun.droidvm.ui.vm.VMActions; -import cn.classfun.droidvm.ui.vm.console.VMConsoleActivity; +import cn.classfun.droidvm.ui.vm.console.VmConsoleRouter; import cn.classfun.droidvm.ui.vm.edit.VMEditActivity; import cn.classfun.droidvm.ui.vm.info.VMInfoActivity; @@ -133,10 +133,9 @@ public void onVMStateChanged(UUID vmId, VMState state) { if (state == VMState.RUNNING && wantOpenConsole.compareAndSet(true, false)) { var config = adapter.items.findById(vmId); if (config == null) return; - var intent = new Intent(requireContext(), VMConsoleActivity.class); - intent.putExtra(VMConsoleActivity.EXTRA_VM_ID, vmId.toString()); - intent.putExtra(VMConsoleActivity.EXTRA_VM_NAME, config.getName()); - startActivity(intent); + // Same routing as the VM-info console button (native -> VNC -> + // serial), so a VNC/native VM auto-opens its display, not always UART. + VmConsoleRouter.openDefault(requireContext(), vmId, config, true); } }); } diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/console/VmConsoleRouter.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/console/VmConsoleRouter.java new file mode 100644 index 000000000..923f7de5e --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/console/VmConsoleRouter.java @@ -0,0 +1,104 @@ +package cn.classfun.droidvm.ui.vm.console; + +import android.content.Context; +import android.content.Intent; +import android.os.Handler; +import android.os.Looper; + +import androidx.annotation.NonNull; + +import java.util.ArrayList; +import java.util.UUID; + +import cn.classfun.droidvm.lib.daemon.DaemonConnection; +import cn.classfun.droidvm.lib.store.vm.VMConfig; +import cn.classfun.droidvm.ui.vm.display.nativedisplay.display.VMNativeDisplayActivity; +import cn.classfun.droidvm.ui.vm.display.vnc.base.BaseVncActivity; +import cn.classfun.droidvm.ui.vm.display.vnc.display.VMVncDisplayActivity; +import cn.classfun.droidvm.ui.vm.display.vnc.display.VMVncPresentationActivity; + +/** + * Shared "open the VM's default view" routing, so the VM-info screen (a console + * button) and the VM-list auto-open-after-start pick the same thing: + * native display, else VNC, else the serial console (uart, then stdio). Keeping + * this in one place is why both paths agree instead of the list always opening + * the UART console regardless of the VM's display. + */ +public final class VmConsoleRouter { + private VmConsoleRouter() { + } + + /** + * Open the VM's default view. {@code running} = the VM is up (so the serial + * console shows the live stream, not saved logs). + */ + public static void openDefault(@NonNull Context ctx, @NonNull UUID vmId, + @NonNull VMConfig config, boolean running) { + var item = config.item; + if (item.optBoolean("native_display_enabled", false)) { + openNative(ctx, vmId, config); + return; + } + if (item.optBoolean("vnc_enabled", false)) { + openVnc(ctx, vmId, config); + return; + } + // Serial console: ask the daemon which streams exist, prefer uart then stdio. + DaemonConnection.getInstance().buildRequest("vm_console_list") + .put("vm_id", vmId.toString()) + .onResponse(resp -> { + var data = resp.optJSONArray("data"); + String stream = null; + if (data != null) { + var names = new ArrayList(); + for (int i = 0; i < data.length(); i++) { + var n = data.optString(i, ""); + if (!n.isEmpty()) names.add(n); + } + if (names.contains("uart")) stream = "uart"; + else if (names.contains("stdio")) stream = "stdio"; + } + if (stream == null) return; + final var s = stream; + new Handler(Looper.getMainLooper()).post( + () -> openConsole(ctx, vmId, config, s, !running)); + }) + .onUnsuccessful(r -> { + }) + .invoke(); + } + + public static void openConsole(@NonNull Context ctx, @NonNull UUID vmId, + @NonNull VMConfig config, @NonNull String stream, boolean logs) { + var intent = new Intent(ctx, VMConsoleActivity.class); + intent.putExtra(VMConsoleActivity.EXTRA_VM_ID, vmId.toString()); + intent.putExtra(VMConsoleActivity.EXTRA_VM_NAME, config.getName()); + intent.putExtra(VMConsoleActivity.EXTRA_STREAM, stream); + intent.putExtra(VMConsoleActivity.EXTRA_LOGS, logs); + ctx.startActivity(intent); + } + + public static void openNative(@NonNull Context ctx, @NonNull UUID vmId, @NonNull VMConfig config) { + var item = config.item; + var intent = new Intent(ctx, VMNativeDisplayActivity.class); + intent.putExtra(VMNativeDisplayActivity.EXTRA_VM_ID, vmId.toString()); + intent.putExtra(VMNativeDisplayActivity.EXTRA_VM_NAME, config.getName()); + intent.putExtra(VMNativeDisplayActivity.EXTRA_WIDTH, item.optLong("display_width", 1280)); + intent.putExtra(VMNativeDisplayActivity.EXTRA_HEIGHT, item.optLong("display_height", 720)); + ctx.startActivity(intent); + } + + public static void openVnc(@NonNull Context ctx, @NonNull UUID vmId, @NonNull VMConfig config) { + var intent = new Intent(ctx, VMVncDisplayActivity.class); + intent.putExtra(BaseVncActivity.EXTRA_VM_ID, vmId.toString()); + intent.putExtra(BaseVncActivity.EXTRA_VM_NAME, config.getName()); + ctx.startActivity(intent); + } + + public static void openVncExt(@NonNull Context ctx, @NonNull UUID vmId, @NonNull VMConfig config) { + var intent = new Intent(ctx, VMVncPresentationActivity.class); + intent.putExtra(BaseVncActivity.EXTRA_VM_ID, vmId.toString()); + intent.putExtra(BaseVncActivity.EXTRA_VM_NAME, config.getName()); + ctx.startActivity(intent); + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/info/ConsoleButton.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/info/ConsoleButton.java index 14f46022a..70597d311 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/info/ConsoleButton.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/info/ConsoleButton.java @@ -3,7 +3,6 @@ import static android.widget.Toast.LENGTH_SHORT; import android.content.DialogInterface; -import android.content.Intent; import android.os.Handler; import android.os.Looper; import android.util.Log; @@ -23,11 +22,7 @@ import cn.classfun.droidvm.lib.store.base.DataItem; import cn.classfun.droidvm.lib.store.vm.VMState; import cn.classfun.droidvm.lib.ui.IconItemAdapter; -import cn.classfun.droidvm.ui.vm.console.VMConsoleActivity; -import cn.classfun.droidvm.ui.vm.display.nativedisplay.display.VMNativeDisplayActivity; -import cn.classfun.droidvm.ui.vm.display.vnc.base.BaseVncActivity; -import cn.classfun.droidvm.ui.vm.display.vnc.display.VMVncDisplayActivity; -import cn.classfun.droidvm.ui.vm.display.vnc.display.VMVncPresentationActivity; +import cn.classfun.droidvm.ui.vm.console.VmConsoleRouter; public final class ConsoleButton { private static final String TAG = "VMInfoActivity"; @@ -54,37 +49,11 @@ void showConsoleChooser() { } void openDefaultConsole() { - if (parent.config != null - && parent.config.item.optBoolean("native_display_enabled", false)) { - openNativeDisplay(); - return; - } - if (parent.config != null && parent.config.item.optBoolean("vnc_enabled", false)) { - openVncDisplay(); - return; - } - DaemonConnection.OnResponse res = resp -> { - var data = resp.optJSONArray("data"); - if (data == null) return; - var list = new ArrayList(); - for (int i = 0; i < data.length(); i++) { - var name = data.optString(i, ""); - if (!name.isEmpty()) list.add(name); - } - String defaultConsole = null; - if (list.contains("uart")) - defaultConsole = "uart"; - if (defaultConsole == null && list.contains("stdio")) - defaultConsole = "stdio"; - if (defaultConsole == null) return; - final var target = defaultConsole; - mainHandler.post(() -> openConsole(target)); - }; - DaemonConnection.getInstance().buildRequest("vm_console_list") - .put("vm_id", parent.vmId.toString()) - .onResponse(res) - .onUnsuccessful(r -> {}) - .invoke(); + if (parent.config == null) return; + // Shared with the VM-list auto-open-after-start (VmConsoleRouter), so both + // pick the same view: native -> VNC -> serial (uart, then stdio). + VmConsoleRouter.openDefault(parent, parent.vmId, parent.config, + parent.currentState != VMState.STOPPED); } private void buildConsoleChooserDialog(@Nullable JSONArray streams) { @@ -144,38 +113,22 @@ private void buildConsoleChooserDialog(@Nullable JSONArray streams) { } private void openConsole(@NonNull String stream) { - var intent = new Intent(parent, VMConsoleActivity.class); - intent.putExtra(VMConsoleActivity.EXTRA_VM_ID, parent.vmId.toString()); - intent.putExtra(VMConsoleActivity.EXTRA_VM_NAME, parent.config.getName()); - intent.putExtra(VMConsoleActivity.EXTRA_STREAM, stream); - intent.putExtra(VMConsoleActivity.EXTRA_LOGS, parent.currentState == VMState.STOPPED); - parent.startActivity(intent); + VmConsoleRouter.openConsole(parent, parent.vmId, parent.config, stream, + parent.currentState == VMState.STOPPED); } private void openNativeDisplay() { if (parent.config == null) return; - var item = parent.config.item; - var intent = new Intent(parent, VMNativeDisplayActivity.class); - intent.putExtra(VMNativeDisplayActivity.EXTRA_VM_ID, parent.vmId.toString()); - intent.putExtra(VMNativeDisplayActivity.EXTRA_VM_NAME, parent.config.getName()); - intent.putExtra(VMNativeDisplayActivity.EXTRA_WIDTH, item.optLong("display_width", 1280)); - intent.putExtra(VMNativeDisplayActivity.EXTRA_HEIGHT, item.optLong("display_height", 720)); - parent.startActivity(intent); + VmConsoleRouter.openNative(parent, parent.vmId, parent.config); } private void openVncDisplay() { if (parent.config == null) return; - var intent = new Intent(parent, VMVncDisplayActivity.class); - intent.putExtra(BaseVncActivity.EXTRA_VM_ID, parent.vmId.toString()); - intent.putExtra(BaseVncActivity.EXTRA_VM_NAME, parent.config.getName()); - parent.startActivity(intent); + VmConsoleRouter.openVnc(parent, parent.vmId, parent.config); } private void openVncExtDisplay() { if (parent.config == null) return; - var intent = new Intent(parent, VMVncPresentationActivity.class); - intent.putExtra(BaseVncActivity.EXTRA_VM_ID, parent.vmId.toString()); - intent.putExtra(BaseVncActivity.EXTRA_VM_NAME, parent.config.getName()); - parent.startActivity(intent); + VmConsoleRouter.openVncExt(parent, parent.vmId, parent.config); } } From 1668dc3ab9f907d4fcc366ea8efd7282584fc9d4 Mon Sep 17 00:00:00 2001 From: HuJK Date: Sun, 5 Jul 2026 22:20:43 +0800 Subject: [PATCH 084/182] network: watchdog re-asserts IP forwarding while an L3 bridge runs netd owns net.ipv4.ip_forward and net.ipv6.conf.all.forwarding globally and clears them whenever it retunes tethering, silently black-holing every forwarded guest packet. IptablesBackend enables them once at init; DefaultRouterWatcher now re-asserts them on its 5s tick for as long as a running LINUX + L3 network needs kernel forwarding. Gated on hasL3LinuxBridge() -- the same condition the routing rules use -- so with no L3 bridge netd's value is left alone rather than forcing global forwarding on the phone. Both families are covered because netd flips v4 and v6 together. Reads before writing via runQuiet so the poll stays out of logcat; only a value netd actually reset is corrected and logged. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01TaHcvnHaZk8qqpKiS25Hbk --- .../network/backend/DefaultRouterWatcher.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/DefaultRouterWatcher.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/DefaultRouterWatcher.java index b17f26bec..ac7a6e04d 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/DefaultRouterWatcher.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/DefaultRouterWatcher.java @@ -24,6 +24,7 @@ import cn.classfun.droidvm.lib.store.network.BridgeType; import cn.classfun.droidvm.lib.store.network.NetworkState; import cn.classfun.droidvm.lib.store.network.UplinkMode; +import cn.classfun.droidvm.lib.utils.RunUtils; public final class DefaultRouterWatcher { private static final String TAG = "DefaultRouterWatcher"; @@ -253,6 +254,63 @@ private synchronized void runOnce() { ensureHostIpMonitor(); updateHostIps(); updateDefaultRouter(); + reassertForwarding(); + } + + /** + * Re-asserts the kernel forwarding sysctls that guest L3 routing depends + * on. netd owns these globally and clears them whenever it retunes + * tethering, which silently black-holes every forwarded guest packet. + * {@link cn.classfun.droidvm.daemon.network.backend.iptables.IptablesBackend} + * enables them once at init; this keeps them enabled for as long as a + * Linux-bridge L3 network is running. + * + *

Gated on {@link #hasL3LinuxBridge()} -- the same "running LINUX + L3" + * condition the routing rules use. With no such network there is nothing + * to forward, so netd's value is left alone rather than forcing global + * forwarding on the phone. Both families are covered because netd flips + * v4 and v6 together (as does {@code enableIpForward}). Reads before + * writing: an untouched value costs one cheap cat and stays silent; only a + * value netd actually reset is corrected and logged (it means netd raced + * us). Never throws out of the tick -- a transient shell failure must not + * tear down the routing rules. + */ + private void reassertForwarding() { + try { + if (!hasL3LinuxBridge()) return; + ensureSysctlOne("/proc/sys/net/ipv4/ip_forward", "net.ipv4.ip_forward"); + ensureSysctlOne("/proc/sys/net/ipv6/conf/all/forwarding", + "net.ipv6.conf.all.forwarding"); + } catch (Exception e) { + Log.w(TAG, "Failed to re-assert forwarding sysctls", e); + } + } + + /** + * Writes 1 to {@code path} only if it is not already 1; logs corrections. + * Both the read and the write go through {@code runQuiet} so the 5s poll + * stays out of logcat -- {@code RunContext.run} would log a "Running + * command: cat ..." line every tick. The only line this ever emits is the + * warning below, and only when netd actually reset the value. + */ + private void ensureSysctlOne(@NonNull String path, @NonNull String name) { + var cur = RunUtils.runQuiet("cat " + path).getOutString(); + if ("1".equals(cur)) return; + Log.w(TAG, fmt("%s=%s (netd reset it); re-enabling forwarding", + name, cur.isEmpty() ? "?" : cur)); + RunUtils.runQuiet("echo 1 > " + path); + } + + /** True while any RUNNING Linux-bridge L3 network needs kernel forwarding. */ + private boolean hasL3LinuxBridge() { + var found = new boolean[]{false}; + context.getNetworks().forEach((uuid, inst) -> { + if (inst.getState() == NetworkState.RUNNING + && inst.getBridgeType() == BridgeType.LINUX + && inst.getUplinkMode() == UplinkMode.L3) + found[0] = true; + }); + return found[0]; } private void updateDefaultRouter() { From e9f6bd586015322ab9143fa0c486dd0ad3455332 Mon Sep 17 00:00:00 2001 From: HuJK Date: Sun, 5 Jul 2026 22:20:49 +0800 Subject: [PATCH 085/182] ci: add non-ASCII and translation-completeness PR checks Two Python checks wired into a new Checks workflow (push + PR): - check_ascii.py: fails on any non-ASCII byte in a tracked file, so a stray smart-quote, em-dash, arrow or CJK char can't slip into code, comments or config. Genuine i18n / natural-language files are exempt via scripts/ci/ascii_allow.txt (translations, privacy text, docs, ...). - check_translations.py: fails if any values-/strings.xml is missing, has extra, or has stale entries vs the translatable keys in values/strings.xml, so nothing ships half-translated. Fixes so the current tree passes: - Replace the check/cross emoji in the virtualization setup step with inline tinted vector icons (ImageSpan over ic_check/ic_close); widen showDetail() to CharSequence; add status_supported/status_unsupported colors. The mode menu already used circle icons; only a stale glyph comment remained. - Convert typographic dashes/arrows/minus-sign and stray CJK in the hugepage and Try comments to ASCII. - Add the 4 missing zh-rTW strings (vm_exit_crash, vm_exit_guest_panic, vm_exit_watchdog, vm_rebooting). Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01TaHcvnHaZk8qqpKiS25Hbk --- .github/workflows/checks.yml | 32 +++++ .../droidvm/ui/hugepage/HugePageActivity.java | 6 +- .../droidvm/ui/hugepage/HugePageModel.java | 24 ++-- .../cn/classfun/droidvm/ui/hugepage/Try.java | 6 +- .../ui/setup/base/BaseCheckStepFragment.java | 2 +- .../step/VirtualizationStepFragment.java | 42 +++++-- .../main/res/menu/menu_hugepage_process.xml | 10 +- app/src/main/res/values-zh-rTW/strings.xml | 4 + app/src/main/res/values/colors.xml | 4 + scripts/ci/ascii_allow.txt | 24 ++++ scripts/ci/check_ascii.py | 111 ++++++++++++++++++ scripts/ci/check_translations.py | 94 +++++++++++++++ 12 files changed, 329 insertions(+), 30 deletions(-) create mode 100644 .github/workflows/checks.yml create mode 100644 scripts/ci/ascii_allow.txt create mode 100644 scripts/ci/check_ascii.py create mode 100644 scripts/ci/check_translations.py diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml new file mode 100644 index 000000000..49354306b --- /dev/null +++ b/.github/workflows/checks.yml @@ -0,0 +1,32 @@ +name: Checks + +on: + push: + branches: [ "**" ] + pull_request: + branches: [ "**" ] + workflow_dispatch: + +concurrency: + group: checks-${{ github.ref }} + cancel-in-progress: true + +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + # No submodules: these checks only look at this repo's own sources. + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: No non-ASCII in code/config + run: python3 scripts/ci/check_ascii.py + + - name: Translations complete across all locales + run: python3 scripts/ci/check_translations.py diff --git a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageActivity.java index a4d4be4cb..be7a6929f 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageActivity.java @@ -160,8 +160,8 @@ private void initialize() { btnViewProcesses.setOnClickListener(v -> startActivity( new Intent(this, HugePageProcessActivity.class))); // Acquire-mode slots: each button starts its mode; each spinner (shown - // while a run is in flight) interrupts it. Listeners are static — the - // slots aren't recycled — and the idle/running visibility toggle is + // while a run is in flight) interrupts it. Listeners are static -- the + // slots aren't recycled -- and the idle/running visibility toggle is // driven by applyAcquireState(). // Short-press runs it (gated to the pressable state); long-press opens the // what-does-this-do dialog with a Run/Cancel choice (always available, even @@ -212,7 +212,7 @@ static void showAcquireInfo(@NonNull Context ctx, int mode, @NonNull Runnable on int msg = mode == 2 ? R.string.hugepage_acquire_v2_explain : mode == 3 ? R.string.hugepage_acquire_v3_explain : R.string.hugepage_acquire_v1_explain; - // Title = "Acquire huge pages" + the mode badge, e.g. "獲取大頁 v1". + // Title = "Acquire huge pages" + the mode badge, e.g. "Acquire huge pages v1". String title = ctx.getString(R.string.hugepage_acquire_pages_title) + " " + ctx.getString(modeLabel); new MaterialAlertDialogBuilder(ctx) diff --git a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageModel.java b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageModel.java index 8c4b5e9ca..cd4d58344 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageModel.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageModel.java @@ -31,17 +31,17 @@ * *

It hides the {@code gh_hugepage_reserve} module's v6/v7 differences: * callers ask for a high-level operation and never branch on version or poke raw - * sysfs. Version is never decided ahead of time — each mutation is a + * sysfs. Version is never decided ahead of time -- each mutation is a * degradation ladder that just tries the best knob and falls back - * (e.g. the migrating {@code acquire} knob → {@code manual_refill}; - * {@code pool_want=0} soft-disable → {@code rmmod}). The internal {@link Try} + * (e.g. the migrating {@code acquire} knob -> {@code manual_refill}; + * {@code pool_want=0} soft-disable -> {@code rmmod}). The internal {@link Try} * ladder is collapsed to a small {@link Result} ({@code OK / UNSUPPORTED / - * FAILED}) for the GUI — that is the "無法執行就回傳 error" contract. + * FAILED}) for the GUI -- that is the "return an error when it can't run" contract. * *

Reads are cached with short TTLs so the per-second refresh loop doesn't * block on the daemon or re-read a static knob every tick. Each screen owns its * own instance (separate caches); everything here is context-free (pure data) and - * never holds an Activity reference. Mutations do shell I/O — call them off the UI + * never holds an Activity reference. Mutations do shell I/O -- call them off the UI * thread. */ final class HugePageModel { @@ -157,7 +157,7 @@ static final class UsageEntry { * Version-unified snapshot of module + pool state. {@code targetIdeal} is the * one "target" concept across versions: v7's {@code pool_want}, else v6's * read-only {@code pool_target}, else current capacity. {@code deficit} is what - * is still missing toward it ({@code targetIdeal − free − lent}); the acquire + * is still missing toward it ({@code targetIdeal - free - lent}); the acquire * buttons are usable exactly when {@code loaded && deficit > 0}. */ static final class Snapshot { @@ -169,7 +169,7 @@ static final class Snapshot { final long built; // pool_total (capacity assembled) final long free; // pool_avail (in the pool now) final long lent; // served (out to VMs); 0 if the module can't report it (v6) - final long deficit; // max(0, targetIdeal − free − lent) + final long deficit; // max(0, targetIdeal - free - lent) final boolean acquiring; // an acquire worker is running final int acquireMode; // which mode (1/2/3), or -1 if the module can't report it final boolean hasPoolWant; // pool_want knob reported (v7 runtime-resizable) @@ -275,7 +275,7 @@ boolean koAvailable() { * Save a new pool target of {@code pages}. Persists it to settings.prop (for * the next boot) and applies it to the running pool if the live {@code pool_want} * knob exists; on v6 (read-only {@code pool_target}) only the persist takes - * effect. Does not fire an acquire — the caller drives that from the + * effect. Does not fire an acquire -- the caller drives that from the * resulting {@link Snapshot#deficit}. */ @NonNull @@ -295,10 +295,10 @@ Result saveSize(long pages) { * Bring the running pool up (true) or down (false), choosing the deepest action * the module supports: *

    - *
  • enable: not loaded → insmod (targeting the saved size); loaded → + *
  • enable: not loaded -> insmod (targeting the saved size); loaded -> * restore the target by writing the saved {@code pool_want} (v6 loaded is * already active, so this is a no-op there);
  • - *
  • disable: try {@code pool_want=0} (soft — frees the reserve but + *
  • disable: try {@code pool_want=0} (soft -- frees the reserve but * keeps per-VM tracking); if that write can't happen (v6 has no such knob) * fall back to {@code rmmod}.
  • *
@@ -310,7 +310,7 @@ Result setRuntimeEnabled(boolean on) { return collapse(loadLadder(savedWantPages())); // insmod } // Loaded: restore the target. Succeeds on v7; on v6 there's no pool_want - // and a loaded module is already active, so treat a failed write as "已啟用". + // and a loaded module is already active, so treat a failed write as "already enabled". var t = writeKnob("pool_want", Long.toString(savedWantPages())); return t.ok() ? Result.ok("soft-enable") : Result.ok("already-active"); } @@ -346,7 +346,7 @@ Result setBootEnabled(boolean on) { * (1 = original scan, 2 = migrate + system reclaim, 3 = migrate + per-block * evict). Prefers the migrating {@code acquire} knob; on any failure (v6 has no * such knob, or -ENOSYS where this kernel can't run that mode) degrades to - * {@code manual_refill}. Fire-and-return — poll {@link #state()} for completion. + * {@code manual_refill}. Fire-and-return -- poll {@link #state()} for completion. */ @NonNull Result acquire(int mode) { diff --git a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/Try.java b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/Try.java index 84df9409a..3f787d527 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/Try.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/Try.java @@ -9,9 +9,9 @@ * accumulates into an ordered {@code List>}; a success stops the ladder, * so the caller judges the overall outcome from the last element: *
    - *
  • last {@link #ok()} and alone — success via the primary implementation;
  • - *
  • last {@link #ok()} after earlier fails — success, degraded to a fallback;
  • - *
  • last not {@link #ok()} — failure, and every rung's reason is in the list.
  • + *
  • last {@link #ok()} and alone -- success via the primary implementation;
  • + *
  • last {@link #ok()} after earlier fails -- success, degraded to a fallback;
  • + *
  • last not {@link #ok()} -- failure, and every rung's reason is in the list.
  • *
* * @param the feature's implementation enum (which way it was done) diff --git a/app/src/main/java/cn/classfun/droidvm/ui/setup/base/BaseCheckStepFragment.java b/app/src/main/java/cn/classfun/droidvm/ui/setup/base/BaseCheckStepFragment.java index 2e44a05ac..eb9c89104 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/setup/base/BaseCheckStepFragment.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/setup/base/BaseCheckStepFragment.java @@ -83,7 +83,7 @@ private void requestSkip() { .show(); } - protected void showDetail(String text) { + protected void showDetail(CharSequence text) { tvStepDetail.setText(text); tvStepDetail.setVisibility(VISIBLE); } diff --git a/app/src/main/java/cn/classfun/droidvm/ui/setup/step/VirtualizationStepFragment.java b/app/src/main/java/cn/classfun/droidvm/ui/setup/step/VirtualizationStepFragment.java index 8a8aee400..5003a803b 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/setup/step/VirtualizationStepFragment.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/setup/step/VirtualizationStepFragment.java @@ -5,14 +5,24 @@ import static cn.classfun.droidvm.lib.utils.ThreadUtils.threadSleep; import static cn.classfun.droidvm.ui.setup.SetupActivity.CHECK_DELAY; +import android.content.Context; +import android.graphics.drawable.Drawable; import android.os.Bundle; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.style.ImageSpan; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import androidx.annotation.ColorRes; +import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; + +import java.util.Objects; import cn.classfun.droidvm.R; import cn.classfun.droidvm.lib.store.vm.VMHypervisor; @@ -46,15 +56,17 @@ private void operationThread() { threadSleep(CHECK_DELAY); boolean success = false; var ctx = requireContext(); - var displayString = new StringBuilder(); + // Per-hypervisor line with an inline check/cross icon (not an emoji) for + // its supported state; a Spannable carries the ImageSpans into the detail + // TextView. Built off the UI thread -- ImageSpan only holds a Drawable, it + // touches no view -- then handed to showDetail() on the UI thread. + var detail = new SpannableStringBuilder(); for (var hyp : VMHypervisor.values()) { if (hyp.getDevicePath() == null) continue; var supported = hyp.isSupported(); - displayString.append(fmt( - "%s: %s\n", - hyp.getDisplayString(ctx), - supported ? "✅" : "❌" - )); + if (detail.length() > 0) detail.append('\n'); + detail.append(hyp.getDisplayString(ctx)).append(": "); + appendStatusIcon(detail, ctx, supported); Log.i(TAG, fmt( "%s: %s", hyp.name().toLowerCase(), @@ -70,10 +82,26 @@ private void operationThread() { } else { showError(R.string.setup_virt_title, R.string.setup_virt_fail); } - showDetail(displayString.toString()); + showDetail(detail); }); } + /** Appends a tinted check (supported) or cross (unsupported) icon inline. */ + private static void appendStatusIcon( + @NonNull SpannableStringBuilder sb, @NonNull Context ctx, boolean supported + ) { + @DrawableRes int icon = supported ? R.drawable.ic_check : R.drawable.ic_close; + @ColorRes int tint = supported ? R.color.status_supported : R.color.status_unsupported; + Drawable d = Objects.requireNonNull(ContextCompat.getDrawable(ctx, icon)).mutate(); + int size = Math.round(ctx.getResources().getDisplayMetrics().density * 16); + d.setBounds(0, 0, size, size); + d.setTint(ContextCompat.getColor(ctx, tint)); + int start = sb.length(); + sb.append(' '); // placeholder glyph the span draws over + sb.setSpan(new ImageSpan(d, ImageSpan.ALIGN_CENTER), + start, sb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + @Override public boolean isHiddenStep() { return !optBoolean("isRoot", false); diff --git a/app/src/main/res/menu/menu_hugepage_process.xml b/app/src/main/res/menu/menu_hugepage_process.xml index 706b70adc..a80f7d10c 100644 --- a/app/src/main/res/menu/menu_hugepage_process.xml +++ b/app/src/main/res/menu/menu_hugepage_process.xml @@ -2,10 +2,12 @@ - + 虛擬機器"%1$s"已停止。 虛擬機器"%1$s"以退出碼 %2$d 退出。 + 虛擬機器"%1$s"已當機(退出碼 %2$d)。 + 虛擬機器"%1$s"guest 核心 panic(退出碼 %2$d)。 + 虛擬機器"%1$s"停滯,已被看門狗重置(退出碼 %2$d)。 + 正在重新啟動虛擬機器"%1$s"… 虛擬機器"%1$s"啟動失敗 虛擬機器以退出碼 %1$d 退出。\n\n日誌: 無可用日誌。 diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index d0bb4de94..665b2beb4 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -25,4 +25,8 @@ #33808080 #807D9CB3 + + + #FF4CAF50 + #FFF44336 diff --git a/scripts/ci/ascii_allow.txt b/scripts/ci/ascii_allow.txt new file mode 100644 index 000000000..976cf50c0 --- /dev/null +++ b/scripts/ci/ascii_allow.txt @@ -0,0 +1,24 @@ +# Files permitted to contain non-ASCII bytes. Everything else tracked in the +# repo must be pure 7-bit ASCII (enforced by check_ascii.py). One glob per line; +# blank lines and '#' comments are ignored. Globs are matched against the +# repo-relative path (POSIX '/'), and '*' also spans '/'. +# +# Only genuine natural-language / i18n content belongs here -- source code, +# build files, layouts, manifests and config must stay ASCII. + +# UI strings: translation source (values/) and the translations themselves. +app/src/main/res/values*/strings.xml + +# Privacy policy, one file per language. +app/src/main/assets/privacy/*.txt + +# Localized data: repo-listing display names and language endonyms. +app/src/main/assets/data/repo.yaml +app/src/main/assets/data/languages.yaml + +# Documentation (prose; may name languages/tools in their own script). +*.md + +# Third-party / tool-generated files we don't hand-edit. +LICENSE.txt +gradlew diff --git a/scripts/ci/check_ascii.py b/scripts/ci/check_ascii.py new file mode 100644 index 000000000..2feb45c94 --- /dev/null +++ b/scripts/ci/check_ascii.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +"""Fail if any tracked text file contains non-ASCII bytes. + +Source code, build files, layouts, manifests and config must be pure 7-bit +ASCII so a stray smart-quote, em-dash, arrow or CJK character can't slip into +code or a comment. Genuine natural-language / i18n content (translations, +privacy text, docs, ...) is exempt via scripts/ci/ascii_allow.txt. + +Scope is the git index (`git ls-files`), so build outputs and submodule +contents are never scanned. Binary files are skipped by extension and by a +NUL-byte sniff. Exit status is 0 when clean, 1 when any violation is found. + +Usage: run from the repo root, or pass the repo root as argv[1]. +""" +import fnmatch +import os +import subprocess +import sys + +ALLOWLIST = "scripts/ci/ascii_allow.txt" + +# Extensions that are always binary -- skipped without reading. +BINARY_EXT = { + ".webp", ".png", ".jpg", ".jpeg", ".gif", ".ico", ".bmp", + ".jar", ".zip", ".zst", ".gz", ".7z", ".apk", ".aar", + ".so", ".a", ".o", ".bin", ".dex", + ".ttf", ".otf", ".woff", ".woff2", + ".keystore", ".jks", ".pdf", +} + +MAX_REPORT_PER_FILE = 5 # don't drown the log on a fully non-ASCII file + + +def load_allow(root): + path = os.path.join(root, ALLOWLIST) + globs = [] + if not os.path.isfile(path): + return globs + with open(path, encoding="utf-8") as fh: + for line in fh: + line = line.split("#", 1)[0].strip() + if line: + globs.append(line) + return globs + + +def allowed(rel, globs): + return any(fnmatch.fnmatch(rel, g) for g in globs) + + +def tracked_files(root): + out = subprocess.check_output( + ["git", "-C", root, "ls-files", "-z"]) + return [p for p in out.decode("utf-8", "surrogateescape").split("\0") if p] + + +def scan(root, rel): + """Return a list of (line, col, char) for non-ASCII bytes, or [].""" + path = os.path.join(root, rel) + if not os.path.isfile(path): # submodule gitlink, symlink to nowhere, ... + return [] + if os.path.splitext(rel)[1].lower() in BINARY_EXT: + return [] + with open(path, "rb") as fh: + data = fh.read() + if b"\x00" in data: # looks binary + return [] + hits = [] + line = col = 1 + for b in data: + if b == 0x0A: + line += 1 + col = 1 + continue + if b > 0x7F: + hits.append((line, col, b)) + col += 1 + return hits + + +def main(): + root = os.path.abspath(sys.argv[1]) if len(sys.argv) > 1 else os.getcwd() + globs = load_allow(root) + violations = 0 + files_with_hits = 0 + for rel in tracked_files(root): + if allowed(rel, globs): + continue + hits = scan(root, rel) + if not hits: + continue + files_with_hits += 1 + violations += len(hits) + print(f"{rel}: {len(hits)} non-ASCII byte(s)") + for line, col, b in hits[:MAX_REPORT_PER_FILE]: + print(f" {rel}:{line}:{col}: byte 0x{b:02X}") + if len(hits) > MAX_REPORT_PER_FILE: + print(f" ... and {len(hits) - MAX_REPORT_PER_FILE} more") + + if violations: + print() + print(f"FAIL: {violations} non-ASCII byte(s) in {files_with_hits} file(s).") + print("Use ASCII (e.g. -- for an em-dash, -> for an arrow), or add a") + print(f"genuinely-i18n path to {ALLOWLIST}.") + return 1 + print("OK: no non-ASCII in tracked code/config files.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/ci/check_translations.py b/scripts/ci/check_translations.py new file mode 100644 index 000000000..7c245a170 --- /dev/null +++ b/scripts/ci/check_translations.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +"""Fail if any locale is missing (or has extra) translated string entries. + +The base `values/strings.xml` is the source of truth: every entry that is not +`translatable="false"` must appear in every `values-/strings.xml`, and a +locale must not carry entries that no longer exist in the base. This keeps all +languages at the same set of keys so nothing ships half-translated. + +Entries compared: , and , keyed by name. (Only +presence of the key is checked, not per-plural quantities -- those legitimately +differ by language.) `values-night` and other non-locale qualifiers are ignored +because they carry no strings.xml. + +Exit status is 0 when every locale matches the base, 1 otherwise. + +Usage: run from the repo root, or pass the res/ directory as argv[1]. +""" +import os +import sys +import xml.etree.ElementTree as ET + +DEFAULT_RES = "app/src/main/res" +ENTRY_TAGS = {"string", "plurals", "string-array"} + + +def names(strings_xml): + """(translatable names, translatable=false names) in one strings.xml.""" + root = ET.parse(strings_xml).getroot() + keep, nontrans = set(), set() + for el in root: + if el.tag not in ENTRY_TAGS: + continue + name = el.get("name") + if not name: + continue + if (el.get("translatable") or "").lower() == "false": + nontrans.add(name) + else: + keep.add(name) + return keep, nontrans + + +def main(): + res = os.path.abspath(sys.argv[1]) if len(sys.argv) > 1 else \ + os.path.join(os.getcwd(), DEFAULT_RES) + + base_xml = os.path.join(res, "values", "strings.xml") + if not os.path.isfile(base_xml): + print(f"FAIL: base strings.xml not found at {base_xml}") + return 1 + base, base_nontrans = names(base_xml) + print(f"base (values/strings.xml): {len(base)} translatable " + f"({len(base_nontrans)} translatable=false)") + + locales = [] + for d in sorted(os.listdir(res)): + if not d.startswith("values-"): + continue + sp = os.path.join(res, d, "strings.xml") + if os.path.isfile(sp): # non-locale qualifiers (e.g. values-night) have none + locales.append((d, sp)) + + if not locales: + print("OK: no translation locales to check.") + return 0 + + failed = False + for d, sp in locales: + loc, _ = names(sp) + missing = sorted(base - loc) + extra = sorted(loc - base) + stale = sorted(loc & base_nontrans) # translating a non-translatable key + if not missing and not extra and not stale: + print(f" OK {d}: {len(loc)}/{len(base)}") + continue + failed = True + print(f" FAIL {d}: {len(loc)}/{len(base)}") + for n in missing: + print(f" missing: {n}") + for n in extra: + print(f" extra: {n} (not in base)") + for n in stale: + print(f" stale: {n} (base marks it translatable=false)") + + if failed: + print() + print("FAIL: translations are out of sync with the base strings.") + return 1 + print("OK: every locale matches the base string set.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 67effe76e61b28add11e13b0e028a743130b5775 Mon Sep 17 00:00:00 2001 From: lateautumn233 Date: Sat, 4 Jul 2026 20:54:04 +0800 Subject: [PATCH 086/182] feat(disk): preserve compression when optimizing images - Fix qemu-img convert: positional args must follow all options (was breaking -c placement) - Add ImageUtils.hasCompressedClusters() using qemu-img check's real compressed-clusters count - Implement applyKeepCompress() to preserve source compression algorithm during optimization - New "Keep compression when optimizing" setting (disabled by default) with UI controls - Add i18n strings (English, Simplified Chinese, Traditional Chinese) --- .../droidvm/lib/utils/ImageUtils.java | 27 +++++++++++++ .../ui/disk/action/DiskActionDialog.java | 1 + .../disk/operation/DiskOperationActivity.java | 40 +++++++++++++++++++ .../disk/operation/ImageCommandGenerate.java | 13 ++++-- .../main/settings/MainSettingsFragment.java | 9 +++++ .../res/layout/fragment_main_settings.xml | 8 ++++ app/src/main/res/values-zh-rCN/strings.xml | 2 + app/src/main/res/values-zh-rTW/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + 9 files changed, 100 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/cn/classfun/droidvm/lib/utils/ImageUtils.java b/app/src/main/java/cn/classfun/droidvm/lib/utils/ImageUtils.java index af4f830a2..9e3b8297d 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/utils/ImageUtils.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/utils/ImageUtils.java @@ -28,4 +28,31 @@ public static JSONObject getImageInfo(String path) throws JSONException { } return new JSONObject(result.getOutString()); } + + /** + * Whether {@code path} actually stores compressed clusters, per + * {@code qemu-img check}'s {@code compressed-clusters} count. This is the + * real signal: the qcow2 header's {@code compression-type} reads "zlib" for + * every v3 image (even ones with no compressed data), so it cannot tell an + * uncompressed disk from a compressed one. Unlike lbx, qemu-img reads both + * zlib and zstd images, so a zstd disk is detected too. Any detection + * failure (raw images reject {@code check}, etc.) returns {@code false} -- + * an undetectable image is treated as uncompressed. + */ + public static boolean hasCompressedClusters(String path) { + try { + var result = runListQuiet( + getPrebuiltBinaryPath("qemu-img"), + "check", "--output=json", path); + var out = result.getOutString(); + // qemu-img check still writes its JSON report on non-zero exits + // (leaks/corruptions), so parse whatever it produced rather than + // gating on isSuccess(); a missing/garbage body throws -> false. + if (out == null || out.isEmpty()) + return false; + return new JSONObject(out).optLong("compressed-clusters", 0) > 0; + } catch (Exception e) { + return false; + } + } } diff --git a/app/src/main/java/cn/classfun/droidvm/ui/disk/action/DiskActionDialog.java b/app/src/main/java/cn/classfun/droidvm/ui/disk/action/DiskActionDialog.java index 56850ee19..99d717265 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/disk/action/DiskActionDialog.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/disk/action/DiskActionDialog.java @@ -121,6 +121,7 @@ public void tryOptimize(@NonNull DiskConfig config) { try { var info = ImageUtils.getImageInfo(config.getFullPath()); obj.put("action", "convert"); + obj.put("keep_compress", true); // preserve compression when optimizing obj.put("format", info.getString("format")); if (info.has("backing-filename")) obj.put("backing_path", info.getString("backing-filename")); diff --git a/app/src/main/java/cn/classfun/droidvm/ui/disk/operation/DiskOperationActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/disk/operation/DiskOperationActivity.java index 12a3b1e54..677cf3b39 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/disk/operation/DiskOperationActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/disk/operation/DiskOperationActivity.java @@ -3,6 +3,8 @@ import static android.view.View.GONE; import static android.view.View.VISIBLE; import static cn.classfun.droidvm.lib.utils.FileUtils.findExecute; +import static cn.classfun.droidvm.lib.utils.ImageUtils.getImageInfo; +import static cn.classfun.droidvm.lib.utils.ImageUtils.hasCompressedClusters; import static cn.classfun.droidvm.lib.utils.ProcessUtils.SIGHUP; import static cn.classfun.droidvm.lib.utils.ProcessUtils.shellKillProcess; import static cn.classfun.droidvm.lib.utils.StringUtils.basename; @@ -42,6 +44,7 @@ import cn.classfun.droidvm.lib.ui.termux.SimpleTerminalSessionClient; import cn.classfun.droidvm.lib.ui.termux.TerminalFonts; import cn.classfun.droidvm.lib.ui.termux.SimpleTerminalViewClient; +import cn.classfun.droidvm.ui.main.settings.MainSettingsFragment; public final class DiskOperationActivity extends AppCompatActivity { private static final String TAG = "DiskOperationActivity"; @@ -133,6 +136,7 @@ public static void startOptimize( try { var obj = new JSONObject(); obj.put("action", "convert"); + obj.put("keep_compress", true); // preserve compression when optimizing var intent = createIntent(context, diskId, obj); context.startActivity(intent); } catch (Exception e) { @@ -221,6 +225,7 @@ public void handleOnBackPressed() { final String cmd; try { var task = new JSONObject(taskJsonStr); + applyKeepCompress(getApplicationContext(), task, diskPath); var gen = new ImageCommandGenerate(diskStore); cmd = gen.buildCommand(task, diskPath); taskAction = task.optString("action", ""); @@ -235,6 +240,41 @@ public void handleOnBackPressed() { }); } + /** + * When a task opts into keep-compress (the user-facing "optimize", not the + * pre-start decompress in {@link #optimizeForResultIntent}), re-compress the + * rewritten image with the algorithm the source uses instead of rewriting it + * uncompressed. Whether the source is compressed at all is decided by + * qemu-img's real {@code compressed-clusters} count + * ({@code ImageUtils.hasCompressedClusters}); the qcow2 header's + * {@code compression-type} is only read to pick zlib vs + * zstd, because that header field reads "zlib" for every v3 image and so + * cannot on its own distinguish an uncompressed disk from a compressed one. + * An uncompressed source (raw, or a v3 qcow2 with no compressed data) is left + * alone. An explicit {@code compress} in the task always wins, the user can + * turn this off globally in settings, and any detection failure falls through + * to the default (uncompressed) rewrite. + */ + private static void applyKeepCompress( + @NonNull Context ctx, @NonNull JSONObject task, @NonNull String path) { + if (!task.optBoolean("keep_compress", false) || task.has("compress")) + return; + if (!MainSettingsFragment.isKeepCompressOnOptimizeEnabled(ctx)) + return; + try { + if (!hasCompressedClusters(path)) + return; // nothing actually compressed -- rewrite uncompressed + var info = getImageInfo(path); + var fmtSpecific = info.optJSONObject("format-specific"); + var data = fmtSpecific == null ? null : fmtSpecific.optJSONObject("data"); + var type = data == null ? "" : data.optString("compression-type", ""); + // qemu compression-type zstd stays zstd; zlib (and anything else) -> deflate. + task.put("compress", "zstd".equals(type) ? "zstd" : "deflate"); + } catch (Exception e) { + Log.w(TAG, "keep-compress detection failed", e); + } + } + private void startTerminalSession(String cmd) { var shell = findExecute("su", "/system/bin/su"); var cwd = getFilesDir().getAbsolutePath(); diff --git a/app/src/main/java/cn/classfun/droidvm/ui/disk/operation/ImageCommandGenerate.java b/app/src/main/java/cn/classfun/droidvm/ui/disk/operation/ImageCommandGenerate.java index 8059020ac..33aaef6c3 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/disk/operation/ImageCommandGenerate.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/disk/operation/ImageCommandGenerate.java @@ -118,7 +118,6 @@ private void appendConvert() throws JSONException { task.put("format", format); } else throw new RuntimeException("No format specified in task or image info"); sb.append(" --target-format ").append(format); - sb.append(" ").append(eDiskPath).append(" "); if (task.has("output")) { outputPath = task.getString("output"); } else { @@ -129,11 +128,17 @@ private void appendConvert() throws JSONException { if (!opts.isEmpty()) sb.append(" --target-format-options ").append(opts); if (!task.optString("compress", "none").equals("none")) - sb.append(" --compress"); - sb.append(" "); + // -c (short form): the bundled qemu-img rejects the --compress long + // option even though it accepts the other long options. + sb.append(" -c"); + // Positional args go last. qemu-img stops parsing options at the first + // SRC_FILE (convert accepts multiple source files), so any option after + // it -- e.g. --compress -- is misread as an extra source file. All + // options must precede SRC_FILE. realPath = escapedString(outputPath); tmpPath = escapedString(fmt("%s.tmp", diskPath)); - sb.append(useTempPath ? tmpPath : realPath); + sb.append(" ").append(eDiskPath); + sb.append(" ").append(useTempPath ? tmpPath : realPath); } @NonNull diff --git a/app/src/main/java/cn/classfun/droidvm/ui/main/settings/MainSettingsFragment.java b/app/src/main/java/cn/classfun/droidvm/ui/main/settings/MainSettingsFragment.java index e65053e55..e09bfb6b2 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/main/settings/MainSettingsFragment.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/main/settings/MainSettingsFragment.java @@ -65,6 +65,7 @@ public final class MainSettingsFragment extends MainBaseFragment { private static final String PREFS_NAME = "droidvm_prefs"; public static final String KEY_VM_AUTO_CONSOLE = "vm_auto_console"; public static final String KEY_VM_CLEAR_LOGS_BEFORE_START = "vm_clear_logs_before_start"; + public static final String KEY_VM_KEEP_COMPRESS_ON_OPTIMIZE = "vm_keep_compress_on_optimize"; public static final String KEY_AUTO_CHECK_UPDATE = "auto_check_update"; private final Handler mainHandler = new Handler(Looper.getMainLooper()); private final Runnable daemonStatusRefreshRunnable = this::periodRefreshDaemonStatus; @@ -78,6 +79,7 @@ public final class MainSettingsFragment extends MainBaseFragment { private TextRowWidget itemDaemonRestart; private SwitchRowWidget itemVMAutoConsole; private SwitchRowWidget itemVMClearLogsBeforeStart; + private SwitchRowWidget itemVMKeepCompressOnOptimize; private TextRowWidget itemLicense; private SwitchRowWidget itemAutoCheckUpdate; private TextRowWidget itemCheckUpdate; @@ -130,6 +132,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat itemDaemonRestart = view.findViewById(R.id.item_daemon_restart); itemVMAutoConsole = view.findViewById(R.id.item_vm_auto_console); itemVMClearLogsBeforeStart = view.findViewById(R.id.item_vm_clear_logs_before_start); + itemVMKeepCompressOnOptimize = view.findViewById(R.id.item_vm_keep_compress_on_optimize); itemLicense = view.findViewById(R.id.item_license); itemAutoCheckUpdate = view.findViewById(R.id.item_auto_check_update); itemCheckUpdate = view.findViewById(R.id.item_check_update); @@ -160,6 +163,7 @@ private void initSettings() { bindOnClick(itemDaemonRestart, daemon::asyncRestartDaemon); bindOnChecked(itemVMAutoConsole, KEY_VM_AUTO_CONSOLE, false); bindOnChecked(itemVMClearLogsBeforeStart, KEY_VM_CLEAR_LOGS_BEFORE_START, false); + bindOnChecked(itemVMKeepCompressOnOptimize, KEY_VM_KEEP_COMPRESS_ON_OPTIMIZE, false); bindOnChecked(itemAutoCheckUpdate, KEY_AUTO_CHECK_UPDATE, true); bindOnClick(itemCheckUpdate, this::checkUpdate); bindOnClick(itemPrivacy, this::showPrivacyPolicy); @@ -203,6 +207,11 @@ public static boolean isClearLogsBeforeStartEnabled(@NonNull Context context) { return prefs.getBoolean(KEY_VM_CLEAR_LOGS_BEFORE_START, false); } + public static boolean isKeepCompressOnOptimizeEnabled(@NonNull Context context) { + var prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + return prefs.getBoolean(KEY_VM_KEEP_COMPRESS_ON_OPTIMIZE, false); + } + public static boolean isAutoCheckUpdateEnabled(@NonNull Context context) { var prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); return prefs.getBoolean(KEY_AUTO_CHECK_UPDATE, true); diff --git a/app/src/main/res/layout/fragment_main_settings.xml b/app/src/main/res/layout/fragment_main_settings.xml index 948cdf8e5..250c048e8 100644 --- a/app/src/main/res/layout/fragment_main_settings.xml +++ b/app/src/main/res/layout/fragment_main_settings.xml @@ -63,6 +63,14 @@ android:subtitle="@string/settings_vm_clear_logs_before_start_summary" android:text="@string/settings_vm_clear_logs_before_start_title" /> + + 启动虚拟机时自动打开控制台 启动前清理日志 启动虚拟机前清理该虚拟机的所有控制台日志 + 优化时保持压缩 + 优化磁盘时保留其原有压缩而非解压;关闭则重写为未压缩 关于 问题反馈 在 GitHub 上报告问题 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index ca90d6224..13af394a9 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -363,6 +363,8 @@ 啟動虛擬機器時自動開啟控制台 啟動前清理日誌 啟動虛擬機器前清理該虛擬機器的所有控制台日誌 + 最佳化時保持壓縮 + 最佳化磁碟時保留其原有壓縮而非解壓縮;關閉則重寫為未壓縮 關於 問題反饋 在 GitHub 上報告問題 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7692a340f..3ca868778 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -369,6 +369,8 @@ Automatically open the console when a VM is started Clear logs before start Clear all console logs for the VM before it starts + Keep compression when optimizing + Preserve a disk\'s existing compression when optimizing instead of decompressing it; off rewrites the disk uncompressed About Feedback Report issues on GitHub From 2ccfec6eb45a418c947d59e34ad6be2290fa9e4f Mon Sep 17 00:00:00 2001 From: lateautumn233 Date: Sun, 5 Jul 2026 22:05:15 +0800 Subject: [PATCH 087/182] feat(disk): add compression option to convert dialog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add compression selector (visible only for QCOW2 format) - Allow QCOW2→QCOW2 conversion to support re-compression/decompression - Handle in-place re-compress via temp file path to avoid read lock collision --- .../ui/disk/action/DiskSetFormatDialog.java | 24 +++++++++++++++++-- .../disk/operation/DiskOperationActivity.java | 5 +++- .../disk/operation/ImageCommandGenerate.java | 5 ++++ .../res/layout/dialog_disk_set_format.xml | 8 +++++++ 4 files changed, 39 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/cn/classfun/droidvm/ui/disk/action/DiskSetFormatDialog.java b/app/src/main/java/cn/classfun/droidvm/ui/disk/action/DiskSetFormatDialog.java index 37043d3e8..358f48696 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/disk/action/DiskSetFormatDialog.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/disk/action/DiskSetFormatDialog.java @@ -7,6 +7,10 @@ import static cn.classfun.droidvm.lib.utils.ThreadUtils.runOnPool; import static cn.classfun.droidvm.ui.disk.operation.DiskOperationActivity.startConvert; +import static android.view.View.GONE; +import static android.view.View.VISIBLE; +import static cn.classfun.droidvm.lib.store.disk.DiskConfig.supportsCompress; + import android.content.Context; import android.os.Handler; import android.os.Looper; @@ -23,6 +27,7 @@ import cn.classfun.droidvm.R; import cn.classfun.droidvm.lib.store.disk.DiskConfig; import cn.classfun.droidvm.lib.utils.ImageUtils; +import cn.classfun.droidvm.ui.disk.create.DiskCompress; import cn.classfun.droidvm.ui.disk.create.DiskFormat; import cn.classfun.droidvm.ui.widgets.row.ChooseRowWidget; import cn.classfun.droidvm.ui.widgets.row.TextInputRowWidget; @@ -34,6 +39,7 @@ public final class DiskSetFormatDialog { private final DiskConfig config; private TextInputRowWidget inputName; private ChooseRowWidget chooseFormat; + private ChooseRowWidget chooseCompress; private TextView tvCurrentFormat; private DiskFormat currentFormat = null; @@ -47,14 +53,18 @@ public void show() { tvCurrentFormat = view.findViewById(R.id.tv_current_format); inputName = view.findViewById(R.id.input_name); chooseFormat = view.findViewById(R.id.choose_format); + chooseCompress = view.findViewById(R.id.choose_compress); chooseFormat.setItems(DiskFormat.class); + chooseCompress.configure(DiskCompress.class, DiskCompress.DEFLATE); inputName.setText(config.getName()); tvCurrentFormat.setText(R.string.disk_set_format_loading); runOnPool(this::loadImageInfo); chooseFormat.setOnValueChangedListener(() -> { DiskFormat fmt = chooseFormat.getSelectedItem(); updateFileName(inputName, config.getName(), fmt.getExt()); + updateCompressVisibility(fmt); }); + updateCompressVisibility(chooseFormat.getSelectedItem()); var dialog = new MaterialAlertDialogBuilder(context) .setTitle(R.string.disk_set_format_title) .setView(view) @@ -70,16 +80,25 @@ public void show() { } inputName.setError(null); DiskFormat format = chooseFormat.getSelectedItem(); - if (format.equals(currentFormat)) { + // Same format is still a valid conversion when it carries a + // compression choice (qcow2): the user may only be re-compressing. + if (format.equals(currentFormat) && !supportsCompress(format)) { Toast.makeText(context, R.string.disk_set_format_error_same, LENGTH_LONG).show(); return; } dialog.dismiss(); var output = pathJoin(config.item.optString("folder", ""), name); - startConvert(context, config.getId(), format.name().toLowerCase(), output); + var compress = supportsCompress(format) + ? chooseCompress.getSelectedItem().name().toLowerCase() + : "none"; + startConvert(context, config.getId(), format.name().toLowerCase(), output, compress); }); } + private void updateCompressVisibility(DiskFormat fmt) { + chooseCompress.setVisibility(supportsCompress(fmt) ? VISIBLE : GONE); + } + private void loadImageInfo() { try { currentFormat = null; @@ -100,6 +119,7 @@ private void loadImageInfo() { currentFormat = chooseFormat.getSelectedItem(); } updateFileName(inputName, config.getName(), currentFormat.getExt()); + updateCompressVisibility(chooseFormat.getSelectedItem()); }); } diff --git a/app/src/main/java/cn/classfun/droidvm/ui/disk/operation/DiskOperationActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/disk/operation/DiskOperationActivity.java index 677cf3b39..5a04126b6 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/disk/operation/DiskOperationActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/disk/operation/DiskOperationActivity.java @@ -148,13 +148,16 @@ public static void startConvert( @NonNull Context context, @NonNull UUID diskId, @NonNull String format, - @NonNull String output + @NonNull String output, + @NonNull String compress ) { try { var obj = new JSONObject(); obj.put("action", "convert"); obj.put("format", format); obj.put("output", output); + if (!compress.equals("none")) + obj.put("compress", compress); var intent = createIntent(context, diskId, obj); context.startActivity(intent); } catch (Exception e) { diff --git a/app/src/main/java/cn/classfun/droidvm/ui/disk/operation/ImageCommandGenerate.java b/app/src/main/java/cn/classfun/droidvm/ui/disk/operation/ImageCommandGenerate.java index 33aaef6c3..157ae6f97 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/disk/operation/ImageCommandGenerate.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/disk/operation/ImageCommandGenerate.java @@ -120,6 +120,11 @@ private void appendConvert() throws JSONException { sb.append(" --target-format ").append(format); if (task.has("output")) { outputPath = task.getString("output"); + // In-place re-compress (output == source): writing straight to the + // target collides with the source's own read lock, so route through + // the .tmp + rename path even though an output was given. + if (outputPath.equals(diskPath)) + useTempPath = true; } else { useTempPath = true; } diff --git a/app/src/main/res/layout/dialog_disk_set_format.xml b/app/src/main/res/layout/dialog_disk_set_format.xml index eeca2d5dc..e6cf78467 100644 --- a/app/src/main/res/layout/dialog_disk_set_format.xml +++ b/app/src/main/res/layout/dialog_disk_set_format.xml @@ -31,4 +31,12 @@ android:icon="@drawable/ic_disk_format" android:text="@string/disk_set_format_target" /> + + From aecff06bbfd4dd64eef0d8b950b5fbf160fc3b95 Mon Sep 17 00:00:00 2001 From: lateautumn233 Date: Sun, 5 Jul 2026 23:37:28 +0800 Subject: [PATCH 088/182] feat(cpu): add CPU affinity support for disk operations - New CpuUtils utility to enumerate cores and identify frequency tiers - Add CPU affinity settings UI with multi-core selection dialog - Bind disk operations (qemu-img, clone) to selected cores via taskset - Support automatic big-core filtering on heterogeneous CPU layouts - Reserve KEY_CROSVM_CPU_AFFINITY for future VM affinity support --- .../vm/backend/CrosvmBackendInstance.java | 5 + .../classfun/droidvm/lib/utils/CpuUtils.java | 214 ++++++++++++++++++ .../disk/operation/DiskOperationActivity.java | 2 + .../disk/operation/ImageCommandGenerate.java | 17 ++ .../main/settings/MainSettingsFragment.java | 106 +++++++++ .../res/layout/fragment_main_settings.xml | 8 + app/src/main/res/values-zh-rCN/strings.xml | 7 + app/src/main/res/values-zh-rTW/strings.xml | 7 + app/src/main/res/values/strings.xml | 7 + 9 files changed, 373 insertions(+) create mode 100644 app/src/main/java/cn/classfun/droidvm/lib/utils/CpuUtils.java diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackendInstance.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackendInstance.java index f83505f23..756bba52b 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackendInstance.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackendInstance.java @@ -135,6 +135,11 @@ private List buildCommand() { var item = config.item; var args = new ArrayList(); prepareStraceArguments(args); + // TODO(cpu-affinity): to bind the VM to selected host cores, prepend a + // system-taskset wrapper here (mirroring prepareStraceArguments): + // "taskset -a ", with the mask from CpuUtils.coresCsvToHexMask + // reading a future MainSettingsFragment.KEY_CROSVM_CPU_AFFINITY setting. + // Note toybox taskset takes a hex mask (no "0x"), not a -c core list. args.add(getPrebuiltBinaryPath("crosvm")); // Top-level flag (must precede `run`): makes crosvm surface CommandStatus // exit codes (see CrosvmExit) so runVM() can tell reset/crash/panic apart. diff --git a/app/src/main/java/cn/classfun/droidvm/lib/utils/CpuUtils.java b/app/src/main/java/cn/classfun/droidvm/lib/utils/CpuUtils.java new file mode 100644 index 000000000..fdb94c03a --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/lib/utils/CpuUtils.java @@ -0,0 +1,214 @@ +package cn.classfun.droidvm.lib.utils; + +import android.util.Log; + +import androidx.annotation.NonNull; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.TreeSet; +import java.util.regex.Pattern; + +/** + * Host CPU topology helper: enumerates cores, reads each core's max frequency + * from sysfs and groups them into frequency tiers so callers can tell the + * little cluster apart from the big/prime clusters (for CPU-affinity binding). + * Reads are best-effort; a core whose frequency cannot be read is reported with + * {@code maxFreqKHz == 0} and treated as tier 0. + */ +public final class CpuUtils { + private static final String TAG = "CpuUtils"; + private static final String CPU_ROOT = "/sys/devices/system/cpu"; + private static final Pattern CPU_DIR = Pattern.compile("cpu\\d+"); + + private CpuUtils() { + } + + /** A single host CPU core and its cluster classification. */ + public static final class CpuCore { + public final int index; + public final long maxFreqKHz; // 0 when unknown + public final int tier; // 0 = lowest-freq cluster, ascending + public final boolean big; // true when not in the lowest-freq cluster + + CpuCore(int index, long maxFreqKHz, int tier, boolean big) { + this.index = index; + this.maxFreqKHz = maxFreqKHz; + this.tier = tier; + this.big = big; + } + } + + /** + * Enumerate host cores sorted by index, each tagged with its frequency tier. + * Never returns null; falls back to {@link Runtime#availableProcessors()} + * with unknown frequencies if sysfs cannot be read. + */ + @NonNull + public static List getCores() { + var indices = listCoreIndices(); + var freqs = new long[indices.size()]; + var distinct = new TreeSet(); + for (int i = 0; i < indices.size(); i++) { + freqs[i] = readMaxFreqKHz(indices.get(i)); + if (freqs[i] > 0) distinct.add(freqs[i]); + } + // Ascending tier index per distinct frequency; unknown (0) stays tier 0. + var tierOf = new ArrayList<>(distinct); + var cores = new ArrayList(indices.size()); + for (int i = 0; i < indices.size(); i++) { + int tier = freqs[i] > 0 ? tierOf.indexOf(freqs[i]) : 0; + cores.add(new CpuCore(indices.get(i), freqs[i], tier, tier > 0)); + } + return cores; + } + + /** Number of distinct frequency clusters (1 when frequencies are unknown). */ + public static int tierCount(@NonNull List cores) { + int max = 0; + for (var c : cores) max = Math.max(max, c.tier); + return max + 1; + } + + /** + * Default selection for "filter out the little cores": every core that is + * not in the lowest-frequency cluster. When there is only a single cluster + * (or detection failed) all cores are returned, since filtering is moot. + */ + @NonNull + public static String defaultBigCoresCsv() { + var cores = getCores(); + boolean single = tierCount(cores) <= 1; + var sb = new StringBuilder(); + for (var c : cores) { + if (single || c.big) { + if (sb.length() > 0) sb.append(','); + sb.append(c.index); + } + } + return sb.toString(); + } + + /** Format a KHz frequency as e.g. "2.60 GHz"; empty string when unknown. */ + @NonNull + public static String formatFreq(long khz) { + if (khz <= 0) return ""; + return String.format(Locale.US, "%.2f GHz", khz / 1_000_000.0); + } + + /** + * Collapse a core-index CSV into a compact range string, e.g. + * "4,5,6,7" -> "4-7", "0,4,5,6,7" -> "0,4-7". Returns "" for empty input. + */ + @NonNull + public static String compactRanges(@NonNull String csv) { + var nums = parseCsv(csv); + if (nums.isEmpty()) return ""; + var sb = new StringBuilder(); + int start = nums.get(0), prev = start; + for (int i = 1; i <= nums.size(); i++) { + int cur = i < nums.size() ? nums.get(i) : Integer.MIN_VALUE; + if (cur == prev + 1) { + prev = cur; + continue; + } + if (sb.length() > 0) sb.append(','); + if (start == prev) sb.append(start); + else sb.append(start).append('-').append(prev); + start = prev = cur; + } + return sb.toString(); + } + + /** + * Convert a core-index CSV into the hex CPU mask that toybox {@code taskset} + * expects (no "0x" prefix), e.g. "4,5,6,7" -> "f0", "0,1" -> "3". Returns + * "" for empty input. Bit N set means core N is allowed. + */ + @NonNull + public static String coresCsvToHexMask(@NonNull String csv) { + var nums = parseCsv(csv); + if (nums.isEmpty()) return ""; + var mask = java.math.BigInteger.ZERO; + for (int idx : nums) { + if (idx >= 0) mask = mask.setBit(idx); + } + return mask.signum() == 0 ? "" : mask.toString(16); + } + + @NonNull + private static List parseCsv(@NonNull String csv) { + var out = new ArrayList(); + if (csv.isEmpty()) return out; + for (var part : csv.split(",")) { + part = part.trim(); + if (part.isEmpty()) continue; + try { + out.add(Integer.parseInt(part)); + } catch (NumberFormatException ignored) { + } + } + return out; + } + + @NonNull + private static List listCoreIndices() { + var out = new ArrayList(); + var root = new File(CPU_ROOT); + var dirs = root.listFiles(); + if (dirs != null) { + for (var d : dirs) { + if (!d.isDirectory() || !CPU_DIR.matcher(d.getName()).matches()) continue; + try { + out.add(Integer.parseInt(d.getName().substring(3))); + } catch (NumberFormatException ignored) { + } + } + } + if (out.isEmpty()) { + // sysfs unreadable -- fall back to the runtime-reported count. + int n = Math.max(1, Runtime.getRuntime().availableProcessors()); + for (int i = 0; i < n; i++) out.add(i); + } else { + out.sort(Integer::compareTo); + } + return out; + } + + private static long readMaxFreqKHz(int index) { + // cpuinfo_max_freq is the hardware ceiling; scaling_max_freq is the + // policy ceiling (usually equal). Try the direct read first, then a + // root-backed read, before giving up on this core. + long v = tryReadFreq(fmt("%s/cpu%d/cpufreq/cpuinfo_max_freq", CPU_ROOT, index)); + if (v > 0) return v; + return tryReadFreq(fmt("%s/cpu%d/cpufreq/scaling_max_freq", CPU_ROOT, index)); + } + + private static long tryReadFreq(@NonNull String path) { + String raw = null; + try { + raw = FileUtils.readFile(path); + } catch (Exception directFailed) { + try { + raw = FileUtils.shellReadFile(path); + } catch (Exception rootFailed) { + Log.d(TAG, "freq read failed: " + path); + } + } + if (raw == null) return 0; + raw = raw.trim(); + if (raw.isEmpty()) return 0; + try { + return Long.parseLong(raw); + } catch (NumberFormatException e) { + return 0; + } + } + + @NonNull + private static String fmt(@NonNull String f, Object... args) { + return String.format(Locale.US, f, args); + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/ui/disk/operation/DiskOperationActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/disk/operation/DiskOperationActivity.java index 5a04126b6..6edf43ac9 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/disk/operation/DiskOperationActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/disk/operation/DiskOperationActivity.java @@ -230,6 +230,8 @@ public void handleOnBackPressed() { var task = new JSONObject(taskJsonStr); applyKeepCompress(getApplicationContext(), task, diskPath); var gen = new ImageCommandGenerate(diskStore); + gen.setCpuAffinity( + MainSettingsFragment.getQemuImgCpuAffinity(getApplicationContext())); cmd = gen.buildCommand(task, diskPath); taskAction = task.optString("action", ""); outputPath = gen.getOutputPath(); diff --git a/app/src/main/java/cn/classfun/droidvm/ui/disk/operation/ImageCommandGenerate.java b/app/src/main/java/cn/classfun/droidvm/ui/disk/operation/ImageCommandGenerate.java index 157ae6f97..825285c46 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/disk/operation/ImageCommandGenerate.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/disk/operation/ImageCommandGenerate.java @@ -14,6 +14,7 @@ import java.util.UUID; import cn.classfun.droidvm.lib.store.disk.DiskStore; +import cn.classfun.droidvm.lib.utils.CpuUtils; import cn.classfun.droidvm.ui.disk.create.DiskFormat; public final class ImageCommandGenerate { @@ -25,6 +26,7 @@ public final class ImageCommandGenerate { private String realPath; private String tmpPath; private String outputPath; + private String cpuAffinity = ""; private final DiskStore diskStore; public ImageCommandGenerate(@NonNull DiskStore diskStore) { @@ -35,6 +37,14 @@ public String getOutputPath() { return outputPath; } + /** + * Restrict the worker (qemu-img, or pv for clone) to the given host cores. + * Value is a taskset -c list such as "4,5,6,7"; empty means no binding. + */ + public void setCpuAffinity(String cpuAffinity) { + this.cpuAffinity = cpuAffinity == null ? "" : cpuAffinity; + } + @NonNull public String buildCommand(@NonNull JSONObject task, String diskPath) throws JSONException { this.task = task; @@ -50,6 +60,13 @@ public String buildCommand(@NonNull JSONObject task, String diskPath) throws JSO sb.append("set -x; "); sb.append(fmt("mkdir -pv %s; ", escapedString(dirname(diskPath)))); sb.append("if "); + // Bind the worker to selected host cores. The prefix goes before both + // the qemu-img and the clone/pv binary, so it covers every action. + // Uses Android's system taskset (toybox), which takes a hex CPU mask + // with no "0x" prefix -- the bundled busybox has no taskset applet. + var affinityMask = CpuUtils.coresCsvToHexMask(cpuAffinity); + if (!affinityMask.isEmpty()) + sb.append("taskset ").append(affinityMask).append(" "); if (!task.getString("action").equals("clone")) sb.append(escapedString(qemuImg)); appendAction(); diff --git a/app/src/main/java/cn/classfun/droidvm/ui/main/settings/MainSettingsFragment.java b/app/src/main/java/cn/classfun/droidvm/ui/main/settings/MainSettingsFragment.java index e09bfb6b2..cd5b6173d 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/main/settings/MainSettingsFragment.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/main/settings/MainSettingsFragment.java @@ -50,6 +50,7 @@ import cn.classfun.droidvm.lib.store.network.NetworkStore; import cn.classfun.droidvm.lib.store.vm.VMStore; import cn.classfun.droidvm.lib.ui.UIContext; +import cn.classfun.droidvm.lib.utils.CpuUtils; import cn.classfun.droidvm.ui.hugepage.HugePageActivity; import cn.classfun.droidvm.ui.main.base.MainBaseFragment; import cn.classfun.droidvm.ui.setup.SetupActivity; @@ -66,6 +67,10 @@ public final class MainSettingsFragment extends MainBaseFragment { public static final String KEY_VM_AUTO_CONSOLE = "vm_auto_console"; public static final String KEY_VM_CLEAR_LOGS_BEFORE_START = "vm_clear_logs_before_start"; public static final String KEY_VM_KEEP_COMPRESS_ON_OPTIMIZE = "vm_keep_compress_on_optimize"; + public static final String KEY_QEMU_IMG_CPU_AFFINITY = "qemu_img_cpu_affinity"; + // Reserved for a future crosvm/qemu VM affinity setting; not read or + // written yet, and intentionally has no UI in this release. + // public static final String KEY_CROSVM_CPU_AFFINITY = "crosvm_cpu_affinity"; public static final String KEY_AUTO_CHECK_UPDATE = "auto_check_update"; private final Handler mainHandler = new Handler(Looper.getMainLooper()); private final Runnable daemonStatusRefreshRunnable = this::periodRefreshDaemonStatus; @@ -80,6 +85,7 @@ public final class MainSettingsFragment extends MainBaseFragment { private SwitchRowWidget itemVMAutoConsole; private SwitchRowWidget itemVMClearLogsBeforeStart; private SwitchRowWidget itemVMKeepCompressOnOptimize; + private TextRowWidget itemCpuAffinity; private TextRowWidget itemLicense; private SwitchRowWidget itemAutoCheckUpdate; private TextRowWidget itemCheckUpdate; @@ -133,6 +139,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat itemVMAutoConsole = view.findViewById(R.id.item_vm_auto_console); itemVMClearLogsBeforeStart = view.findViewById(R.id.item_vm_clear_logs_before_start); itemVMKeepCompressOnOptimize = view.findViewById(R.id.item_vm_keep_compress_on_optimize); + itemCpuAffinity = view.findViewById(R.id.item_cpu_affinity); itemLicense = view.findViewById(R.id.item_license); itemAutoCheckUpdate = view.findViewById(R.id.item_auto_check_update); itemCheckUpdate = view.findViewById(R.id.item_check_update); @@ -164,6 +171,8 @@ private void initSettings() { bindOnChecked(itemVMAutoConsole, KEY_VM_AUTO_CONSOLE, false); bindOnChecked(itemVMClearLogsBeforeStart, KEY_VM_CLEAR_LOGS_BEFORE_START, false); bindOnChecked(itemVMKeepCompressOnOptimize, KEY_VM_KEEP_COMPRESS_ON_OPTIMIZE, false); + bindOnClick(itemCpuAffinity, this::showCpuAffinityDialog); + refreshCpuAffinitySummary(); bindOnChecked(itemAutoCheckUpdate, KEY_AUTO_CHECK_UPDATE, true); bindOnClick(itemCheckUpdate, this::checkUpdate); bindOnClick(itemPrivacy, this::showPrivacyPolicy); @@ -217,6 +226,16 @@ public static boolean isAutoCheckUpdateEnabled(@NonNull Context context) { return prefs.getBoolean(KEY_AUTO_CHECK_UPDATE, true); } + /** + * qemu-img CPU affinity as a taskset -c core list (e.g. "4,5,6,7"). + * Empty string means no binding (qemu-img may use every core). + */ + @NonNull + public static String getQemuImgCpuAffinity(@NonNull Context context) { + var prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + return prefs.getString(KEY_QEMU_IMG_CPU_AFFINITY, ""); + } + private void onRefreshDaemonStatus(boolean r) { itemDaemonStatus.setSubtitle(r ? R.string.settings_daemon_running : R.string.settings_daemon_stopped); @@ -276,6 +295,93 @@ private void showLanguageDialog() { .show(); } + private void refreshCpuAffinitySummary() { + if (itemCpuAffinity == null) return; + var csv = getQemuImgCpuAffinity(requireContext()); + if (csv.isEmpty()) { + itemCpuAffinity.setSubtitle(R.string.settings_cpu_affinity_all_cores); + } else { + itemCpuAffinity.setSubtitle(getString( + R.string.settings_cpu_affinity_summary_fmt, CpuUtils.compactRanges(csv))); + } + } + + private void showCpuAffinityDialog() { + var ctx = requireContext(); + var cores = CpuUtils.getCores(); + int tiers = CpuUtils.tierCount(cores); + var labels = new String[cores.size()]; + for (int i = 0; i < cores.size(); i++) + labels[i] = cpuCoreLabel(cores.get(i), tiers); + + // Saved selection, or the "filter out little cores" default when unset. + var savedCsv = getQemuImgCpuAffinity(ctx); + var selectedIdx = parseCsvToSet( + savedCsv.isEmpty() ? CpuUtils.defaultBigCoresCsv() : savedCsv); + var checked = new boolean[cores.size()]; + for (int i = 0; i < cores.size(); i++) + checked[i] = selectedIdx.contains(cores.get(i).index); + + var dialog = new MaterialAlertDialogBuilder(ctx) + .setTitle(R.string.settings_cpu_affinity_title) + .setMultiChoiceItems(labels, checked, (d, which, isChecked) -> + checked[which] = isChecked) + .setNeutralButton(R.string.settings_cpu_affinity_big_only, null) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(android.R.string.ok, (d, w) -> { + var sb = new StringBuilder(); + for (int i = 0; i < cores.size(); i++) { + if (!checked[i]) continue; + if (sb.length() > 0) sb.append(','); + sb.append(cores.get(i).index); + } + ctx.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + .edit().putString(KEY_QEMU_IMG_CPU_AFFINITY, sb.toString()).apply(); + refreshCpuAffinitySummary(); + }) + .create(); + dialog.show(); + // Re-check only the big cores without dismissing the dialog. + dialog.getButton(android.app.AlertDialog.BUTTON_NEUTRAL).setOnClickListener(v -> { + var bigIdx = parseCsvToSet(CpuUtils.defaultBigCoresCsv()); + var list = dialog.getListView(); + for (int i = 0; i < cores.size(); i++) { + checked[i] = bigIdx.contains(cores.get(i).index); + list.setItemChecked(i, checked[i]); + } + }); + } + + @NonNull + private String cpuCoreLabel(@NonNull CpuUtils.CpuCore core, int tiers) { + var freq = CpuUtils.formatFreq(core.maxFreqKHz); + int tierRes; + if (tiers <= 1) tierRes = 0; + else if (core.tier == 0) tierRes = R.string.settings_cpu_affinity_tier_little; + else if (tiers >= 3 && core.tier == tiers - 1) + tierRes = R.string.settings_cpu_affinity_tier_prime; + else tierRes = R.string.settings_cpu_affinity_tier_big; + var sb = new StringBuilder(fmt("CPU%d", core.index)); + if (!freq.isEmpty()) sb.append(" ").append(freq); + if (tierRes != 0) sb.append(" ").append(getString(tierRes)); + return sb.toString(); + } + + @NonNull + private static java.util.Set parseCsvToSet(@NonNull String csv) { + var set = new java.util.HashSet(); + if (csv.isEmpty()) return set; + for (var part : csv.split(",")) { + part = part.trim(); + if (part.isEmpty()) continue; + try { + set.add(Integer.parseInt(part)); + } catch (NumberFormatException ignored) { + } + } + return set; + } + private void showLicenseDialog() { new SoftwareLicenseDialog(requireContext()).show(); } diff --git a/app/src/main/res/layout/fragment_main_settings.xml b/app/src/main/res/layout/fragment_main_settings.xml index 250c048e8..5deeeb484 100644 --- a/app/src/main/res/layout/fragment_main_settings.xml +++ b/app/src/main/res/layout/fragment_main_settings.xml @@ -71,6 +71,14 @@ android:subtitle="@string/settings_vm_keep_compress_on_optimize_summary" android:text="@string/settings_vm_keep_compress_on_optimize_title" /> + + 启动虚拟机前清理该虚拟机的所有控制台日志 优化时保持压缩 优化磁盘时保留其原有压缩而非解压;关闭则重写为未压缩 + CPU 亲和性 + 磁盘操作运行于核心 %1$s + 磁盘操作可使用全部核心 + 仅大核 + 小核 + 大核 + 超大核 关于 问题反馈 在 GitHub 上报告问题 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 13af394a9..a12832af4 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -365,6 +365,13 @@ 啟動虛擬機器前清理該虛擬機器的所有控制台日誌 最佳化時保持壓縮 最佳化磁碟時保留其原有壓縮而非解壓縮;關閉則重寫為未壓縮 + CPU 親和性 + 磁碟操作執行於核心 %1$s + 磁碟操作可使用全部核心 + 僅大核 + 小核 + 大核 + 超大核 關於 問題反饋 在 GitHub 上報告問題 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3ca868778..f8b427d6f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -371,6 +371,13 @@ Clear all console logs for the VM before it starts Keep compression when optimizing Preserve a disk\'s existing compression when optimizing instead of decompressing it; off rewrites the disk uncompressed + CPU affinity + Disk ops run on cores %1$s + Disk ops may use all cores + Big cores only + little + big + prime About Feedback Report issues on GitHub From 88cd342e500ed7e4cc7c338a8afe8daf782ee3ee Mon Sep 17 00:00:00 2001 From: HuJK Date: Mon, 6 Jul 2026 13:28:02 +0800 Subject: [PATCH 089/182] review (PR #34): rename VMConsoleRouter, relocate Try, harden shell calls, ASCII base strings Addresses BigfootACA's review comments: - Rename VmConsoleRouter -> VMConsoleRouter (VM initialism); update references. - Move the generic Try degradation-ladder helper to lib/utils for reuse (now public; javadoc de-specialised from HugePageModel). - HugePageModel: wrap the module path in escapedString(); build strings with fmt() instead of + concatenation. - HugePageActivity: fmt() for the acquire-dialog title. - DefaultRouterWatcher: read the sysctl via shellReadFile(). - Base values/strings.xml is ASCII-only now (typographic ellipsis/em-dash -> "..." / "-"), and the non-ASCII allowlist narrows to values-*/strings.xml so the English source file is checked too. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01TaHcvnHaZk8qqpKiS25Hbk --- .../network/backend/DefaultRouterWatcher.java | 18 ++++++++------- .../{ui/hugepage => lib/utils}/Try.java | 22 +++++++++---------- .../droidvm/ui/hugepage/HugePageActivity.java | 14 ++++++------ .../droidvm/ui/hugepage/HugePageModel.java | 21 ++++++++++-------- .../droidvm/ui/main/vm/MainVMFragment.java | 4 ++-- ...onsoleRouter.java => VMConsoleRouter.java} | 4 ++-- .../droidvm/ui/vm/info/ConsoleButton.java | 14 ++++++------ app/src/main/res/values/strings.xml | 20 ++++++++--------- scripts/ci/ascii_allow.txt | 5 +++-- 9 files changed, 64 insertions(+), 58 deletions(-) rename app/src/main/java/cn/classfun/droidvm/{ui/hugepage => lib/utils}/Try.java (70%) rename app/src/main/java/cn/classfun/droidvm/ui/vm/console/{VmConsoleRouter.java => VMConsoleRouter.java} (98%) diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/DefaultRouterWatcher.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/DefaultRouterWatcher.java index ac7a6e04d..c6b8e59b2 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/DefaultRouterWatcher.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/DefaultRouterWatcher.java @@ -1,5 +1,6 @@ package cn.classfun.droidvm.daemon.network.backend; +import static cn.classfun.droidvm.lib.utils.FileUtils.shellReadFile; import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; import android.util.Log; @@ -193,7 +194,7 @@ private void reconcileRules(boolean ipv6, @NonNull List tables) { // lowest priority so it is evaluated first. var desired = new ArrayList(); for (int i = 0; i < tables.size(); i++) - desired.add((RULE_PRIORITY_BASE + i) + " " + tables.get(i)); + desired.add(fmt("%d %s", RULE_PRIORITY_BASE + i, tables.get(i))); // Group our installed rules per device as "priority table" specs. Ours // are the plain rules: an iif owned by a bridge, a lookup action, no @@ -214,7 +215,7 @@ private void reconcileRules(boolean ipv6, @NonNull List tables) { if (dev.startsWith(br)) { ours = true; break; } if (!ours) continue; installed.computeIfAbsent(dev, k -> new ArrayList<>()) - .add(r.optInt("priority", 0) + " " + r.optInt("table", 0)); + .add(fmt("%d %d", r.optInt("priority", 0), r.optInt("table", 0))); if (r.optBoolean("detached", false)) detached.add(dev); } installed.values().forEach(specs -> specs.sort(Comparator.comparingInt( @@ -288,17 +289,18 @@ private void reassertForwarding() { /** * Writes 1 to {@code path} only if it is not already 1; logs corrections. - * Both the read and the write go through {@code runQuiet} so the 5s poll - * stays out of logcat -- {@code RunContext.run} would log a "Running - * command: cat ..." line every tick. The only line this ever emits is the - * warning below, and only when netd actually reset the value. + * The read ({@code shellReadFile}) and the write ({@code runQuiet}) are both + * quiet, so the 5s poll stays out of logcat -- {@code RunContext.run} would + * otherwise log a "Running command: cat ..." line every tick. The only line + * this emits on a normal tick is the warning below, and only when netd + * actually reset the value. */ private void ensureSysctlOne(@NonNull String path, @NonNull String name) { - var cur = RunUtils.runQuiet("cat " + path).getOutString(); + var cur = shellReadFile(path); if ("1".equals(cur)) return; Log.w(TAG, fmt("%s=%s (netd reset it); re-enabling forwarding", name, cur.isEmpty() ? "?" : cur)); - RunUtils.runQuiet("echo 1 > " + path); + RunUtils.runQuiet(fmt("echo 1 > %s", path)); } /** True while any RUNNING Linux-bridge L3 network needs kernel forwarding. */ diff --git a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/Try.java b/app/src/main/java/cn/classfun/droidvm/lib/utils/Try.java similarity index 70% rename from app/src/main/java/cn/classfun/droidvm/ui/hugepage/Try.java rename to app/src/main/java/cn/classfun/droidvm/lib/utils/Try.java index 3f787d527..ed195bbb4 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/Try.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/utils/Try.java @@ -1,13 +1,13 @@ -package cn.classfun.droidvm.ui.hugepage; +package cn.classfun.droidvm.lib.utils; import androidx.annotation.Nullable; /** * One implementation's attempt within a degradation ladder: which implementation * ran, plus its return value on success or its failure reason otherwise. The pair - * {@code (value | error, impl)} is what a {@link HugePageModel} feature call - * accumulates into an ordered {@code List>}; a success stops the ladder, - * so the caller judges the overall outcome from the last element: + * {@code (value | error, impl)} is what a feature call accumulates into an ordered + * {@code List>}; a success stops the ladder, so the caller judges the + * overall outcome from the last element: *
    *
  • last {@link #ok()} and alone -- success via the primary implementation;
  • *
  • last {@link #ok()} after earlier fails -- success, degraded to a fallback;
  • @@ -17,15 +17,15 @@ * @param the feature's implementation enum (which way it was done) * @param the value produced on success ({@link Void} for pure actions) */ -final class Try { +public final class Try { /** The implementation this attempt used. */ - final I impl; + public final I impl; /** The value produced on success; {@code null} on failure (or for {@link Void}). */ @Nullable - final V value; + public final V value; /** The failure reason; {@code null} on success. */ @Nullable - final String error; + public final String error; private Try(I impl, @Nullable V value, @Nullable String error) { this.impl = impl; @@ -33,16 +33,16 @@ private Try(I impl, @Nullable V value, @Nullable String error) { this.error = error; } - static Try ok(I impl, @Nullable V value) { + public static Try ok(I impl, @Nullable V value) { return new Try<>(impl, value, null); } - static Try fail(I impl, String error) { + public static Try fail(I impl, String error) { return new Try<>(impl, null, error); } /** True if this implementation succeeded (a non-null error means failure). */ - boolean ok() { + public boolean ok() { return error == null; } } diff --git a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageActivity.java index be7a6929f..bcd32a974 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageActivity.java @@ -6,6 +6,7 @@ import static cn.classfun.droidvm.lib.utils.FileUtils.shellCheckExists; import static cn.classfun.droidvm.lib.utils.FileUtils.shellReadFile; import static cn.classfun.droidvm.lib.utils.RunUtils.runList; +import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; import static cn.classfun.droidvm.lib.utils.StringUtils.pathJoin; import static cn.classfun.droidvm.lib.utils.ThreadUtils.runOnPool; @@ -213,8 +214,9 @@ static void showAcquireInfo(@NonNull Context ctx, int mode, @NonNull Runnable on : mode == 3 ? R.string.hugepage_acquire_v3_explain : R.string.hugepage_acquire_v1_explain; // Title = "Acquire huge pages" + the mode badge, e.g. "Acquire huge pages v1". - String title = ctx.getString(R.string.hugepage_acquire_pages_title) - + " " + ctx.getString(modeLabel); + String title = fmt("%s %s", + ctx.getString(R.string.hugepage_acquire_pages_title), + ctx.getString(modeLabel)); new MaterialAlertDialogBuilder(ctx) .setTitle(title) .setMessage(msg) @@ -365,7 +367,7 @@ private void updateUI( String name = vmMap.get(pid); if (name == null) name = getString(R.string.hugepage_proc_pid, pid); // Two stacked lines: label over capacity. - usedLabels[i] = name + "\n" + SizeUtils.formatSize(ownerPages * PAGE_SIZE); + usedLabels[i] = fmt("%s\n%s", name, SizeUtils.formatSize(ownerPages * PAGE_SIZE)); seg += ownerPages; } // "used" is the sum of the segments the bar draws, so the caption @@ -385,11 +387,9 @@ private void updateUI( setPagesString(tvPoolTotal, R.string.hugepage_stat_pool_total, held); setPagesString(tvPoolSize, R.string.hugepage_stat_pool_size, poolWant); segPoolBar.setStorage(usedColors, usedValues, usedLabels, - poolAvail, getString(R.string.hugepage_bar_available) - + "\n" + SizeUtils.formatSize(poolAvail * PAGE_SIZE), + poolAvail, fmt("%s\n%s", getString(R.string.hugepage_bar_available), SizeUtils.formatSize(poolAvail * PAGE_SIZE)), HugePageColor.pending(this), deficit, - getString(R.string.hugepage_proc_deficit) - + "\n" + SizeUtils.formatSize(deficit * PAGE_SIZE), + fmt("%s\n%s", getString(R.string.hugepage_proc_deficit), SizeUtils.formatSize(deficit * PAGE_SIZE)), poolWant); } else { rowStatState.setValue(getString(R.string.hugepage_stats_unavailable)); diff --git a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageModel.java b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageModel.java index cd4d58344..f26d7e977 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageModel.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageModel.java @@ -2,8 +2,10 @@ import static cn.classfun.droidvm.lib.utils.FileUtils.shellCheckExists; import static cn.classfun.droidvm.lib.utils.FileUtils.shellReadFile; +import static cn.classfun.droidvm.lib.utils.RunUtils.escapedString; import static cn.classfun.droidvm.lib.utils.RunUtils.run; import static cn.classfun.droidvm.lib.utils.RunUtils.runList; +import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; import static cn.classfun.droidvm.lib.utils.StringUtils.pathJoin; import android.os.SystemClock; @@ -24,6 +26,7 @@ import cn.classfun.droidvm.lib.daemon.DaemonConnection; import cn.classfun.droidvm.lib.run.RunResult; +import cn.classfun.droidvm.lib.utils.Try; /** * The single huge-page facade both screens ({@link HugePageActivity}, @@ -365,7 +368,7 @@ Result acquire(int mode) { if (writeKnob("acquire", Integer.toString(m)).ok()) { // impl names the actual migrating mode that ran ("v1".."v3"); // degraded when it's below what was asked for. - return Result.ok("v" + m, m != mode); + return Result.ok(fmt("v%d", m), m != mode); } } var refill = writeKnob("manual_refill", "1"); @@ -428,8 +431,8 @@ private static Usage usageOf(@NonNull List>> log) { @NonNull private List> loadLadder(long pages) { var log = new ArrayList>(); - var w = "pool_want=\"" + pages + "\""; - var t = "pool_target=\"" + pages + "\""; + var w = fmt("pool_want=\"%d\"", pages); + var t = fmt("pool_target=\"%d\"", pages); // Pass BOTH size params first. A lenient kernel silently ignores the param // the module doesn't have, so it still gets the right size via the one it // does - pool_want (v7) or pool_target (v6). This is essential: on a v6 @@ -438,7 +441,7 @@ private List> loadLadder(long pages) { // Strict kernels reject the unknown param, so fall back to each key alone, // then a bare (default-size) load. if (rung(null, log, LoadImpl.BOTH, - () -> insmod(LoadImpl.BOTH, w + " " + t))) return log; + () -> insmod(LoadImpl.BOTH, fmt("%s %s", w, t)))) return log; if (rung(null, log, LoadImpl.POOL_WANT, () -> insmod(LoadImpl.POOL_WANT, w))) return log; if (rung(null, log, LoadImpl.POOL_TARGET, @@ -449,8 +452,8 @@ private List> loadLadder(long pages) { private static Try insmod(@NonNull LoadImpl impl, @Nullable String arg) { var r = (arg == null) - ? run("insmod \"%s\"", KO_PATH) - : run("insmod \"%s\" %s", KO_PATH, arg); + ? run("insmod %s", escapedString(KO_PATH)) + : run("insmod %s %s", escapedString(KO_PATH), arg); return r.isSuccess() ? Try.ok(impl, null) : Try.fail(impl, reason(r, "insmod")); } @@ -511,8 +514,8 @@ private static Try writeSettings(long pages) { private static String reason(@NonNull RunResult r, @NonNull String what) { var msg = r.getErrString().trim(); if (msg.isEmpty()) msg = r.getOutString().trim(); - if (msg.isEmpty()) msg = "exit " + r.getCode(); - return what + ": " + msg; + if (msg.isEmpty()) msg = fmt("exit %d", r.getCode()); + return fmt("%s: %s", what, msg); } /** @@ -619,7 +622,7 @@ private Try> usageFromKo() { out.sort((a, b) -> Long.compare(b.pages, a.pages)); return Try.ok(Source.KO, out); } catch (Exception e) { - return Try.fail(Source.KO, "KO read: " + e.getMessage()); + return Try.fail(Source.KO, fmt("KO read: %s", e.getMessage())); } } diff --git a/app/src/main/java/cn/classfun/droidvm/ui/main/vm/MainVMFragment.java b/app/src/main/java/cn/classfun/droidvm/ui/main/vm/MainVMFragment.java index 442b18a93..e6b56d4e8 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/main/vm/MainVMFragment.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/main/vm/MainVMFragment.java @@ -28,7 +28,7 @@ import cn.classfun.droidvm.lib.store.vm.VMStore; import cn.classfun.droidvm.ui.main.base.stateful.MainStatefulFragment; import cn.classfun.droidvm.ui.vm.VMActions; -import cn.classfun.droidvm.ui.vm.console.VmConsoleRouter; +import cn.classfun.droidvm.ui.vm.console.VMConsoleRouter; import cn.classfun.droidvm.ui.vm.edit.VMEditActivity; import cn.classfun.droidvm.ui.vm.info.VMInfoActivity; @@ -135,7 +135,7 @@ public void onVMStateChanged(UUID vmId, VMState state) { if (config == null) return; // Same routing as the VM-info console button (native -> VNC -> // serial), so a VNC/native VM auto-opens its display, not always UART. - VmConsoleRouter.openDefault(requireContext(), vmId, config, true); + VMConsoleRouter.openDefault(requireContext(), vmId, config, true); } }); } diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/console/VmConsoleRouter.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/console/VMConsoleRouter.java similarity index 98% rename from app/src/main/java/cn/classfun/droidvm/ui/vm/console/VmConsoleRouter.java rename to app/src/main/java/cn/classfun/droidvm/ui/vm/console/VMConsoleRouter.java index 923f7de5e..a8a014634 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/console/VmConsoleRouter.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/console/VMConsoleRouter.java @@ -24,8 +24,8 @@ * this in one place is why both paths agree instead of the list always opening * the UART console regardless of the VM's display. */ -public final class VmConsoleRouter { - private VmConsoleRouter() { +public final class VMConsoleRouter { + private VMConsoleRouter() { } /** diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/info/ConsoleButton.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/info/ConsoleButton.java index 70597d311..5cfda47be 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/info/ConsoleButton.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/info/ConsoleButton.java @@ -22,7 +22,7 @@ import cn.classfun.droidvm.lib.store.base.DataItem; import cn.classfun.droidvm.lib.store.vm.VMState; import cn.classfun.droidvm.lib.ui.IconItemAdapter; -import cn.classfun.droidvm.ui.vm.console.VmConsoleRouter; +import cn.classfun.droidvm.ui.vm.console.VMConsoleRouter; public final class ConsoleButton { private static final String TAG = "VMInfoActivity"; @@ -50,9 +50,9 @@ void showConsoleChooser() { void openDefaultConsole() { if (parent.config == null) return; - // Shared with the VM-list auto-open-after-start (VmConsoleRouter), so both + // Shared with the VM-list auto-open-after-start (VMConsoleRouter), so both // pick the same view: native -> VNC -> serial (uart, then stdio). - VmConsoleRouter.openDefault(parent, parent.vmId, parent.config, + VMConsoleRouter.openDefault(parent, parent.vmId, parent.config, parent.currentState != VMState.STOPPED); } @@ -113,22 +113,22 @@ private void buildConsoleChooserDialog(@Nullable JSONArray streams) { } private void openConsole(@NonNull String stream) { - VmConsoleRouter.openConsole(parent, parent.vmId, parent.config, stream, + VMConsoleRouter.openConsole(parent, parent.vmId, parent.config, stream, parent.currentState == VMState.STOPPED); } private void openNativeDisplay() { if (parent.config == null) return; - VmConsoleRouter.openNative(parent, parent.vmId, parent.config); + VMConsoleRouter.openNative(parent, parent.vmId, parent.config); } private void openVncDisplay() { if (parent.config == null) return; - VmConsoleRouter.openVnc(parent, parent.vmId, parent.config); + VMConsoleRouter.openVnc(parent, parent.vmId, parent.config); } private void openVncExtDisplay() { if (parent.config == null) return; - VmConsoleRouter.openVncExt(parent, parent.vmId, parent.config); + VMConsoleRouter.openVncExt(parent, parent.vmId, parent.config); } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1ab0997f6..64c71f4bf 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -115,7 +115,7 @@ VM \"%1$s\" crashed (code %2$d). VM \"%1$s\" hit a kernel panic in the guest (code %2$d). VM \"%1$s\" stalled and was reset by the watchdog (code %2$d). - Rebooting VM \"%1$s\"… + Rebooting VM \"%1$s\"... VM \"%1$s\" Failed The virtual machine exited with code %1$d.\n\nLogs: No logs available. @@ -972,18 +972,18 @@ Uses alloc_contig_pages to collect huge pages Uses try_to_free_mem_cgroup_pages to push other apps into swap/zram\nthen alloc_contig_range to collect huge pages Uses folio_isolate_lru + reclaim_pages to precisely push pages into swap/zram\nthen alloc_contig_range to collect huge pages - Acquiring huge pages… + Acquiring huge pages... Acquire done - Downgraded — ran %1$s instead + Downgraded - ran %1$s instead Stop acquiring Acquire complete: reached %1$s Acquire stopped: got %1$s of %2$s (system can\'t migrate more right now) - Module short; trying userspace assist… + Module short; trying userspace assist... Acquire huge pages Pool Size Pool size saved (effective after reboot) Pool size saved & applied - Acquiring — can\'t shrink now, wait for it to finish + Acquiring - can\'t shrink now, wait for it to finish Pool can\'t be smaller than the running VMs\' total memory (%1$s) Failed to save pool size Module enabled (effective after reboot) @@ -1006,7 +1006,7 @@ Served: %1$d pages (%2$s) Usage: %1$d pages (%2$s) Unattributed - Unattributed — freed %1$d / reused %2$d pages + Unattributed - freed %1$d / reused %2$d pages Waiting for acquire pid %1$d Available @@ -1020,11 +1020,11 @@ zombie unknown / exited Scan mode - Scanning… + Scanning... Display mode Module attribution (KO) System scan (THP) - Module unavailable — using scan mode + Module unavailable - using scan mode No processes are currently using the reserved hugepage pool. No VM is holding reserved huge pages. No running VM is using huge pages. @@ -1032,8 +1032,8 @@ Loaded module has no per-VM attribution API (old build). Switch to System scan (menu). Kill Kill %1$s? - SIGTERM (15) — graceful - SIGKILL (9) — force + SIGTERM (15) - graceful + SIGKILL (9) - force Signal sent Failed to send signal Kernel stack (pid %1$d) diff --git a/scripts/ci/ascii_allow.txt b/scripts/ci/ascii_allow.txt index 976cf50c0..503a8e61a 100644 --- a/scripts/ci/ascii_allow.txt +++ b/scripts/ci/ascii_allow.txt @@ -6,8 +6,9 @@ # Only genuine natural-language / i18n content belongs here -- source code, # build files, layouts, manifests and config must stay ASCII. -# UI strings: translation source (values/) and the translations themselves. -app/src/main/res/values*/strings.xml +# Translated UI strings only. The base values/strings.xml (English source) must +# stay ASCII -- it is deliberately not matched by this locale-qualified glob. +app/src/main/res/values-*/strings.xml # Privacy policy, one file per language. app/src/main/assets/privacy/*.txt From 21cf41fed1af6f1d68659b492873b3e4d8b7180d Mon Sep 17 00:00:00 2001 From: lateautumn233 Date: Mon, 6 Jul 2026 13:49:52 +0800 Subject: [PATCH 090/182] refactor(disk): address review feedback on CPU affinity and image utils - CrosvmBackendInstance: drop the no-op cpu-affinity TODO comment - CpuUtils: reuse SizeUtils.findFloatUnit for frequency formatting and StringUtils.fmt instead of a local fmt helper - ImageUtils: split out getImageCheck() mirroring getImageInfo() - MainSettingsFragment: import Set/HashSet, drop commented-out reserved key --- .../vm/backend/CrosvmBackendInstance.java | 5 ---- .../classfun/droidvm/lib/utils/CpuUtils.java | 23 +++++++++------- .../droidvm/lib/utils/ImageUtils.java | 26 ++++++++++++------- .../main/settings/MainSettingsFragment.java | 9 +++---- 4 files changed, 34 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackendInstance.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackendInstance.java index 756bba52b..f83505f23 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackendInstance.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackendInstance.java @@ -135,11 +135,6 @@ private List buildCommand() { var item = config.item; var args = new ArrayList(); prepareStraceArguments(args); - // TODO(cpu-affinity): to bind the VM to selected host cores, prepend a - // system-taskset wrapper here (mirroring prepareStraceArguments): - // "taskset -a ", with the mask from CpuUtils.coresCsvToHexMask - // reading a future MainSettingsFragment.KEY_CROSVM_CPU_AFFINITY setting. - // Note toybox taskset takes a hex mask (no "0x"), not a -c core list. args.add(getPrebuiltBinaryPath("crosvm")); // Top-level flag (must precede `run`): makes crosvm surface CommandStatus // exit codes (see CrosvmExit) so runVM() can tell reset/crash/panic apart. diff --git a/app/src/main/java/cn/classfun/droidvm/lib/utils/CpuUtils.java b/app/src/main/java/cn/classfun/droidvm/lib/utils/CpuUtils.java index fdb94c03a..72c09374e 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/utils/CpuUtils.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/utils/CpuUtils.java @@ -1,5 +1,7 @@ package cn.classfun.droidvm.lib.utils; +import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; + import android.util.Log; import androidx.annotation.NonNull; @@ -7,10 +9,11 @@ import java.io.File; import java.util.ArrayList; import java.util.List; -import java.util.Locale; import java.util.TreeSet; import java.util.regex.Pattern; +import cn.classfun.droidvm.lib.size.SizeUtils; + /** * Host CPU topology helper: enumerates cores, reads each core's max frequency * from sysfs and groups them into frequency tiers so callers can tell the @@ -91,11 +94,18 @@ public static String defaultBigCoresCsv() { return sb.toString(); } - /** Format a KHz frequency as e.g. "2.60 GHz"; empty string when unknown. */ + // Hz-family labels indexed by SizeUtils tier (B/KB/MB/GB/... -> Hz/kHz/...). + private static final String[] FREQ_UNITS = {"Hz", "kHz", "MHz", "GHz", "THz"}; + + /** Format a KHz frequency as e.g. "2.42 GHz"; empty string when unknown. */ @NonNull public static String formatFreq(long khz) { if (khz <= 0) return ""; - return String.format(Locale.US, "%.2f GHz", khz / 1_000_000.0); + var pair = SizeUtils.findFloatUnit(khz * 1000L); + int tier = pair.getUnit().ordinal(); + String unit = tier < FREQ_UNITS.length + ? FREQ_UNITS[tier] : pair.getUnit().getString(); + return fmt("%s %s", pair.getNumberFloat(2), unit); } /** @@ -194,7 +204,7 @@ private static long tryReadFreq(@NonNull String path) { try { raw = FileUtils.shellReadFile(path); } catch (Exception rootFailed) { - Log.d(TAG, "freq read failed: " + path); + Log.d(TAG, fmt("freq read failed: %s", path)); } } if (raw == null) return 0; @@ -206,9 +216,4 @@ private static long tryReadFreq(@NonNull String path) { return 0; } } - - @NonNull - private static String fmt(@NonNull String f, Object... args) { - return String.format(Locale.US, f, args); - } } diff --git a/app/src/main/java/cn/classfun/droidvm/lib/utils/ImageUtils.java b/app/src/main/java/cn/classfun/droidvm/lib/utils/ImageUtils.java index 9e3b8297d..ba4d3042c 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/utils/ImageUtils.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/utils/ImageUtils.java @@ -29,6 +29,21 @@ public static JSONObject getImageInfo(String path) throws JSONException { return new JSONObject(result.getOutString()); } + @NonNull + public static JSONObject getImageCheck(String path) throws JSONException { + var result = runListQuiet( + getPrebuiltBinaryPath("qemu-img"), + "check", "--output=json", path + ); + // qemu-img check still writes its JSON report on non-zero exits + // (leaks/corruptions), so parse whatever it produced rather than + // gating on isSuccess(); a missing/garbage body throws. + var out = result.getOutString(); + if (out == null || out.isEmpty()) + throw new JSONException("qemu-img check produced no output"); + return new JSONObject(out); + } + /** * Whether {@code path} actually stores compressed clusters, per * {@code qemu-img check}'s {@code compressed-clusters} count. This is the @@ -41,16 +56,7 @@ public static JSONObject getImageInfo(String path) throws JSONException { */ public static boolean hasCompressedClusters(String path) { try { - var result = runListQuiet( - getPrebuiltBinaryPath("qemu-img"), - "check", "--output=json", path); - var out = result.getOutString(); - // qemu-img check still writes its JSON report on non-zero exits - // (leaks/corruptions), so parse whatever it produced rather than - // gating on isSuccess(); a missing/garbage body throws -> false. - if (out == null || out.isEmpty()) - return false; - return new JSONObject(out).optLong("compressed-clusters", 0) > 0; + return getImageCheck(path).optLong("compressed-clusters", 0) > 0; } catch (Exception e) { return false; } diff --git a/app/src/main/java/cn/classfun/droidvm/ui/main/settings/MainSettingsFragment.java b/app/src/main/java/cn/classfun/droidvm/ui/main/settings/MainSettingsFragment.java index cd5b6173d..72f80ba7d 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/main/settings/MainSettingsFragment.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/main/settings/MainSettingsFragment.java @@ -36,7 +36,9 @@ import java.nio.charset.StandardCharsets; import java.text.SimpleDateFormat; import java.util.Date; +import java.util.HashSet; import java.util.Locale; +import java.util.Set; import cn.classfun.droidvm.BuildConfig; import cn.classfun.droidvm.R; @@ -68,9 +70,6 @@ public final class MainSettingsFragment extends MainBaseFragment { public static final String KEY_VM_CLEAR_LOGS_BEFORE_START = "vm_clear_logs_before_start"; public static final String KEY_VM_KEEP_COMPRESS_ON_OPTIMIZE = "vm_keep_compress_on_optimize"; public static final String KEY_QEMU_IMG_CPU_AFFINITY = "qemu_img_cpu_affinity"; - // Reserved for a future crosvm/qemu VM affinity setting; not read or - // written yet, and intentionally has no UI in this release. - // public static final String KEY_CROSVM_CPU_AFFINITY = "crosvm_cpu_affinity"; public static final String KEY_AUTO_CHECK_UPDATE = "auto_check_update"; private final Handler mainHandler = new Handler(Looper.getMainLooper()); private final Runnable daemonStatusRefreshRunnable = this::periodRefreshDaemonStatus; @@ -368,8 +367,8 @@ else if (tiers >= 3 && core.tier == tiers - 1) } @NonNull - private static java.util.Set parseCsvToSet(@NonNull String csv) { - var set = new java.util.HashSet(); + private static Set parseCsvToSet(@NonNull String csv) { + var set = new HashSet(); if (csv.isEmpty()) return set; for (var part : csv.split(",")) { part = part.trim(); From 4ce83d3ccef3ba45ae779bdf78e8f89885a3da68 Mon Sep 17 00:00:00 2001 From: lateautumn233 Date: Mon, 6 Jul 2026 14:14:57 +0800 Subject: [PATCH 091/182] fix(cpu): format CPU frequency with decimal (SI) units findFloatUnit uses binary 1024 steps, which underreports clock speeds (2.60 GHz shown as 2.42 GHz). Frequencies are SI/decimal, so pick the Hz/kHz/MHz/GHz tier by dividing by 1000. --- .../classfun/droidvm/lib/utils/CpuUtils.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/cn/classfun/droidvm/lib/utils/CpuUtils.java b/app/src/main/java/cn/classfun/droidvm/lib/utils/CpuUtils.java index 72c09374e..571414a45 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/utils/CpuUtils.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/utils/CpuUtils.java @@ -12,8 +12,6 @@ import java.util.TreeSet; import java.util.regex.Pattern; -import cn.classfun.droidvm.lib.size.SizeUtils; - /** * Host CPU topology helper: enumerates cores, reads each core's max frequency * from sysfs and groups them into frequency tiers so callers can tell the @@ -94,18 +92,20 @@ public static String defaultBigCoresCsv() { return sb.toString(); } - // Hz-family labels indexed by SizeUtils tier (B/KB/MB/GB/... -> Hz/kHz/...). + // Frequency uses decimal (SI) steps of 1000, unlike SizeUtils' binary units. private static final String[] FREQ_UNITS = {"Hz", "kHz", "MHz", "GHz", "THz"}; - /** Format a KHz frequency as e.g. "2.42 GHz"; empty string when unknown. */ + /** Format a KHz frequency as e.g. "2.60 GHz"; empty string when unknown. */ @NonNull public static String formatFreq(long khz) { if (khz <= 0) return ""; - var pair = SizeUtils.findFloatUnit(khz * 1000L); - int tier = pair.getUnit().ordinal(); - String unit = tier < FREQ_UNITS.length - ? FREQ_UNITS[tier] : pair.getUnit().getString(); - return fmt("%s %s", pair.getNumberFloat(2), unit); + double value = khz * 1000.0; + int tier = 0; + while (value >= 1000.0 && tier < FREQ_UNITS.length - 1) { + value /= 1000.0; + tier++; + } + return fmt("%.2f %s", value, FREQ_UNITS[tier]); } /** From 6f19517a94e44f1a34d286a4fe0aa9e42f35caf9 Mon Sep 17 00:00:00 2001 From: HuJK Date: Mon, 6 Jul 2026 13:28:09 +0800 Subject: [PATCH 092/182] ci: enforce fmt() over complex string concatenation; convert existing ones Add scripts/ci/check_string_concat.py (wired into the Checks workflow): a Java-literal-aware scanner that fails when a bare + joins a string literal with a runtime expression ("x=" + v), which reads better as fmt("x=%s", v). It masks comments and string/char literals first, so + inside a literal ("wlan+", "%s+", "\s+") and literal+literal constants are not flagged; a line can opt out with a trailing // concat-ok comment. A glance-obvious accumulator append stays as-is: a += statement with at most ONE + (s += "x" + y) reads clearly and is exempt; a += chaining two or more + (s += "a" + b + "c") loses the exemption and is flagged. Only concatenation that doesn't read at a glance is pushed to fmt(). The + count is scoped per statement (to the next ; { }). Convert the pre-existing complex concatenations across the codebase to fmt() (mostly Log calls); route filesystem-path joins through pathJoin(). Simple single-+ += appends are left as +=. StringUtils.pathJoin's own body keeps its + with a // concat-ok note -- it is the canonical joiner and fmt() there would be a hot-path cost. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01TaHcvnHaZk8qqpKiS25Hbk --- .github/workflows/checks.yml | 3 + .../daemon/display/DaemonSystemContext.java | 3 +- .../daemon/display/NativeDisplayBinder.java | 13 +- .../daemon/network/NetworkInstanceStore.java | 6 +- .../backend/gvisor/GvisorBridgeBackend.java | 2 +- .../lib/download/DiskDownloadManager.java | 8 +- .../lib/download/DiskDownloadService.java | 1 + .../lib/store/network/NetworkConfig.java | 5 +- .../lib/store/network/NetworkStore.java | 7 +- .../droidvm/lib/store/vm/NativeDisplay.java | 2 +- .../droidvm/lib/ui/termux/TerminalFonts.java | 3 +- .../droidvm/lib/utils/AssetUtils.java | 2 +- .../droidvm/lib/utils/StringUtils.java | 2 +- .../droidvm/ui/vm/NicLeaseAllocator.java | 3 +- .../droidvm/ui/vm/boot/BootEntries.java | 3 +- .../droidvm/ui/vm/boot/BootMenuDialog.java | 3 +- .../display/DisplayProvider.java | 5 +- .../nativedisplay/input/InputForwarder.java | 4 +- .../droidvm/ui/vm/info/VMInfoActivity.java | 9 +- scripts/ci/check_string_concat.py | 158 ++++++++++++++++++ 20 files changed, 204 insertions(+), 38 deletions(-) create mode 100644 scripts/ci/check_string_concat.py diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 49354306b..78e71579d 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -30,3 +30,6 @@ jobs: - name: Translations complete across all locales run: python3 scripts/ci/check_translations.py + + - name: Use fmt() instead of string concatenation + run: python3 scripts/ci/check_string_concat.py diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/display/DaemonSystemContext.java b/app/src/main/java/cn/classfun/droidvm/daemon/display/DaemonSystemContext.java index f69ec8b44..3437a54aa 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/display/DaemonSystemContext.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/display/DaemonSystemContext.java @@ -1,5 +1,6 @@ package cn.classfun.droidvm.daemon.display; +import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; import android.annotation.SuppressLint; import android.content.Context; import android.os.Looper; @@ -34,7 +35,7 @@ public static synchronized void init() { try { if (Looper.myLooper() == null) Looper.prepareMainLooper(); } catch (Throwable t) { - Log.w(TAG, "prepareMainLooper: " + t); + Log.w(TAG, fmt("prepareMainLooper: %s", t)); } try { Class at = Class.forName("android.app.ActivityThread"); diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/display/NativeDisplayBinder.java b/app/src/main/java/cn/classfun/droidvm/daemon/display/NativeDisplayBinder.java index 97d8ce6d2..6b866490b 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/display/NativeDisplayBinder.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/display/NativeDisplayBinder.java @@ -1,5 +1,6 @@ package cn.classfun.droidvm.daemon.display; +import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; import static cn.classfun.droidvm.BuildConfig.APPLICATION_ID; import android.content.Intent; @@ -78,7 +79,7 @@ private static void broadcast(@NonNull IBinder binder, @NonNull String nonce) { intent.setPackage(APPLICATION_ID); intent.putExtra(NativeDisplay.EXTRA_BUNDLE, bundle); context.sendBroadcast(intent); - Log.i(TAG, "broadcast native-display binder (nonce=" + nonce + ")"); + Log.i(TAG, fmt("broadcast native-display binder (nonce=%s)", nonce)); } private static IBinder smCall(@NonNull String method, @NonNull String name) { @@ -87,14 +88,14 @@ private static IBinder smCall(@NonNull String method, @NonNull String name) { Method m = sm.getMethod(method, String.class); return (IBinder) m.invoke(null, name); } catch (Exception e) { - Log.w(TAG, method + " reflection failed: " + e.getMessage()); + Log.w(TAG, fmt("%s reflection failed: %s", method, e.getMessage())); return null; } } private static IBinder waitForServiceWithTimeout(@NonNull String name, long timeoutMs) { var holder = new IBinder[1]; - var t = new Thread(() -> holder[0] = smCall("waitForService", name), "WaitSvc-" + name); + var t = new Thread(() -> holder[0] = smCall("waitForService", name), fmt("WaitSvc-%s", name)); t.setDaemon(true); t.start(); try { @@ -105,7 +106,7 @@ private static IBinder waitForServiceWithTimeout(@NonNull String name, long time } private static IBinder doWaitForDisplayBinder(@NonNull String serviceName) { - Log.i(TAG, "waitForDisplayBinder('" + serviceName + "')"); + Log.i(TAG, fmt("waitForDisplayBinder('%s')", serviceName)); var direct = smCall("checkService", serviceName); if (direct != null) { Log.i(TAG, "OK: got display binder directly from ServiceManager"); @@ -117,8 +118,8 @@ private static IBinder doWaitForDisplayBinder(@NonNull String serviceName) { Log.i(TAG, "OK: got display binder via waitForService"); return waited; } - Log.e(TAG, "'" + serviceName + "' not found - is crosvm running with " - + "--android-display-service " + serviceName + "?"); + Log.e(TAG, fmt("'%s' not found - is crosvm running with " + + "--android-display-service %s?", serviceName, serviceName)); return null; } } diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/NetworkInstanceStore.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/NetworkInstanceStore.java index f0f4fd5b3..e69a9975d 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/network/NetworkInstanceStore.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/NetworkInstanceStore.java @@ -52,8 +52,7 @@ protected boolean load(@NonNull DataStore store, @NonNull JSONO JsonUtils.forEachArray(obj, getTypeName(), (JSONObject entry) -> { var migrated = NetworkConfig.migrate(entry); if (migrated == null) { - Log.w(TAG, "Skipping network config with unsupported schema: " - + entry.optString("name")); + Log.w(TAG, fmt("Skipping network config with unsupported schema: %s", entry.optString("name"))); return; } if (migrated != entry) { @@ -63,8 +62,7 @@ protected boolean load(@NonNull DataStore store, @NonNull JSONO try { NetworkConfigValidator.validate(new NetworkConfig(migrated)); } catch (Exception e) { - Log.w(TAG, "Skipping legacy network that failed migration/validation: " - + entry.optString("name"), e); + Log.w(TAG, fmt("Skipping legacy network that failed migration/validation: %s", entry.optString("name")), e); return; } } diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/gvisor/GvisorBridgeBackend.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/gvisor/GvisorBridgeBackend.java index 87af1a35d..e6c8bb6cf 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/gvisor/GvisorBridgeBackend.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/gvisor/GvisorBridgeBackend.java @@ -377,7 +377,7 @@ private void postForward( fmt("/api/v1/gateways/%d/forwards", vlanId), rule); if (resp.isSuccess()) { var id = resp.json().optString("id", ""); - if (!id.isEmpty()) ids.add(vlanId + "/" + id); + if (!id.isEmpty()) ids.add(fmt("%d/%s", vlanId, id)); } else { failures.add(fmt("%s %s (%s)", proto, bind, briefBody(resp.body))); } diff --git a/app/src/main/java/cn/classfun/droidvm/lib/download/DiskDownloadManager.java b/app/src/main/java/cn/classfun/droidvm/lib/download/DiskDownloadManager.java index f5547a95d..0d2ff6b21 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/download/DiskDownloadManager.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/download/DiskDownloadManager.java @@ -231,7 +231,7 @@ static void runDownload(@NonNull Context context, long id) { var ctx = context.getApplicationContext(); var d = JOBS.get(id); if (d == null) return; - var part = new File(d.folder, d.name + ".part"); + var part = new File(d.folder, fmt("%s.part", d.name)); var dest = new File(d.folder, d.name); try { var parent = dest.getParentFile(); @@ -285,15 +285,15 @@ private static void downloadOnce(Download d, File part) throws IOException { conn.setInstanceFollowRedirects(true); conn.setRequestProperty("User-Agent", d.userAgent); conn.setRequestProperty("Accept-Encoding", "identity"); - if (existing > 0) conn.setRequestProperty("Range", "bytes=" + existing + "-"); + if (existing > 0) conn.setRequestProperty("Range", fmt("bytes=%d-", existing)); d.activeConn = conn; try { int code = conn.getResponseCode(); boolean resumed = code == HttpURLConnection.HTTP_PARTIAL; // 206 if (code != HttpURLConnection.HTTP_OK && code != HttpURLConnection.HTTP_PARTIAL) - throw new IOException("HTTP " + code); + throw new IOException(fmt("HTTP %d", code)); Log.i(TAG, fmt("Download #%d %s from %s (HTTP %d)", - d.id, existing > 0 ? "resuming@" + existing : "starting", conn.getURL(), code)); + d.id, existing > 0 ? fmt("resuming@%d", existing) : "starting", conn.getURL(), code)); // Server ignored our Range and is sending the whole file again. long start = resumed ? existing : 0; d.total = resolveTotal(conn, resumed, start); diff --git a/app/src/main/java/cn/classfun/droidvm/lib/download/DiskDownloadService.java b/app/src/main/java/cn/classfun/droidvm/lib/download/DiskDownloadService.java index d582423a5..6ea6a7022 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/download/DiskDownloadService.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/download/DiskDownloadService.java @@ -1,5 +1,6 @@ package cn.classfun.droidvm.lib.download; +import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; import static cn.classfun.droidvm.lib.size.SizeUtils.formatSize; import static cn.classfun.droidvm.lib.utils.StringUtils.formatDuration; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/network/NetworkConfig.java b/app/src/main/java/cn/classfun/droidvm/lib/store/network/NetworkConfig.java index 1fdd22675..d44e27afc 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/network/NetworkConfig.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/network/NetworkConfig.java @@ -1,5 +1,6 @@ package cn.classfun.droidvm.lib.store.network; +import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; import android.util.Log; import androidx.annotation.NonNull; @@ -30,7 +31,7 @@ public NetworkConfig() { public NetworkConfig(@NonNull JSONObject obj) throws JSONException { item.set(obj); if (!isSupportedSchema(obj)) throw new JSONException( - "Unsupported network config schema (expected " + SCHEMA_VERSION + ")" + fmt("Unsupported network config schema (expected %s)", SCHEMA_VERSION) ); } @@ -57,7 +58,7 @@ public static JSONObject migrate(@NonNull JSONObject obj) { try { return migrateLegacyToV2(obj); } catch (Exception e) { - Log.w(TAG, "Failed to migrate legacy network config: " + obj.optString("name"), e); + Log.w(TAG, fmt("Failed to migrate legacy network config: %s", obj.optString("name")), e); return null; } } diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/network/NetworkStore.java b/app/src/main/java/cn/classfun/droidvm/lib/store/network/NetworkStore.java index 89d24008e..b2b7f7d83 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/network/NetworkStore.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/network/NetworkStore.java @@ -1,5 +1,6 @@ package cn.classfun.droidvm.lib.store.network; +import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; import android.content.Context; import android.util.Log; @@ -42,8 +43,7 @@ protected boolean load(@NonNull DataStore store, @NonNull JSONObj JsonUtils.forEachArray(obj, getTypeName(), (JSONObject entry) -> { var migrated = NetworkConfig.migrate(entry); if (migrated == null) { - Log.w(TAG, "Skipping network config with unsupported schema: " - + entry.optString("name")); + Log.w(TAG, fmt("Skipping network config with unsupported schema: %s", entry.optString("name"))); return; } if (migrated == entry) { // already current schema @@ -58,8 +58,7 @@ protected boolean load(@NonNull DataStore store, @NonNull JSONObj cfg = new NetworkConfig(migrated); NetworkConfigValidator.validate(cfg); } catch (Exception e) { - Log.w(TAG, "Skipping legacy network that failed migration/validation: " - + entry.optString("name"), e); + Log.w(TAG, fmt("Skipping legacy network that failed migration/validation: %s", entry.optString("name")), e); return; } store.add(cfg); diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/NativeDisplay.java b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/NativeDisplay.java index c6951f64a..b04457605 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/NativeDisplay.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/NativeDisplay.java @@ -50,7 +50,7 @@ public static String serviceName(@NonNull VMConfig config) { /** Same as {@link #serviceName(VMConfig)} but from a raw VM id (e.g. an Intent extra). */ @NonNull public static String serviceNameFromId(@NonNull String vmId) { - return "droidvm_disp_" + sanitize(vmId); + return fmt("droidvm_disp_%s", sanitize(vmId)); } /** The socket path crosvm connects to for [vmKey]'s [channel]. Must match across all callers. */ diff --git a/app/src/main/java/cn/classfun/droidvm/lib/ui/termux/TerminalFonts.java b/app/src/main/java/cn/classfun/droidvm/lib/ui/termux/TerminalFonts.java index d52fe68c7..d6b64d9e2 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/ui/termux/TerminalFonts.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/ui/termux/TerminalFonts.java @@ -1,5 +1,6 @@ package cn.classfun.droidvm.lib.ui.termux; +import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; import android.content.Context; import android.graphics.Typeface; import android.util.Log; @@ -49,7 +50,7 @@ public static Typeface load(@NonNull Context ctx) { try { return Typeface.createFromFile(userFont); } catch (Exception e) { - Log.w(TAG, "Ignoring unreadable user font " + userFont, e); + Log.w(TAG, fmt("Ignoring unreadable user font %s", userFont), e); } } if (bundled == null) { diff --git a/app/src/main/java/cn/classfun/droidvm/lib/utils/AssetUtils.java b/app/src/main/java/cn/classfun/droidvm/lib/utils/AssetUtils.java index a6776b2bd..4b04513c0 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/utils/AssetUtils.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/utils/AssetUtils.java @@ -316,7 +316,7 @@ private static void extractTarXz(@NonNull InputStream rawIn, @NonNull File destR if (isZeroBlock(header)) break; var name = cString(header, 0, 100); var prefix = cString(header, 345, 155); - var path = prefix.isEmpty() ? name : prefix + "/" + name; + var path = prefix.isEmpty() ? name : pathJoin(prefix, name); var size = parseOctal(header, 124, 12); var mode = (int) parseOctal(header, 100, 8); var type = (char) (header[156] & 0xff); diff --git a/app/src/main/java/cn/classfun/droidvm/lib/utils/StringUtils.java b/app/src/main/java/cn/classfun/droidvm/lib/utils/StringUtils.java index f1ab98d08..3d1a4613e 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/utils/StringUtils.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/utils/StringUtils.java @@ -137,7 +137,7 @@ public static String extensionLower(@NonNull String path) { public static String pathJoin(@NonNull String base, @NonNull String child) { if (base.endsWith("/")) base = base.substring(0, base.length() - 1); if (child.startsWith("/")) child = child.substring(1); - return base + "/" + child; + return base + "/" + child; // concat-ok: the canonical path join; fmt() would be a hot-path perf hit } @NonNull diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/NicLeaseAllocator.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/NicLeaseAllocator.java index 58bce9581..018fb1b34 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/NicLeaseAllocator.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/NicLeaseAllocator.java @@ -1,5 +1,6 @@ package cn.classfun.droidvm.ui.vm; +import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; import android.content.Context; import android.util.Log; @@ -60,7 +61,7 @@ public static void resolveAndPersist(@NonNull VMConfig config, @NonNull Context long offset = firstFree(usedOffsets(vmStore, config, selfId, network, vlan), vlan, net4); if (offset < 0) { - Log.w(TAG, "No free DHCPv4 offset for a NIC on network " + netId); + Log.w(TAG, fmt("No free DHCPv4 offset for a NIC on network %s", netId)); return; } nic.setDhcp4Offset(offset); diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/boot/BootEntries.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/boot/BootEntries.java index 54664008f..de833127f 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/boot/BootEntries.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/boot/BootEntries.java @@ -1,5 +1,6 @@ package cn.classfun.droidvm.ui.vm.boot; +import static cn.classfun.droidvm.lib.utils.StringUtils.pathJoin; import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; import android.content.Context; @@ -176,7 +177,7 @@ public static void analyzeUrl( @NonNull Context ctx, @NonNull String url, int timeoutSecs, @NonNull OnProgress onProgress, @NonNull OnScan onScan ) { - var lbx = ctx.getApplicationInfo().nativeLibraryDir + "/liblbx.so"; + var lbx = pathJoin(ctx.getApplicationInfo().nativeLibraryDir, "liblbx.so"); var thread = new Thread( () -> runAnalyze(lbx, url, timeoutSecs, onProgress, onScan), "lbx-analyze"); thread.setDaemon(true); diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/boot/BootMenuDialog.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/boot/BootMenuDialog.java index 238ebd8a4..cd84b5bb4 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/boot/BootMenuDialog.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/boot/BootMenuDialog.java @@ -1,5 +1,6 @@ package cn.classfun.droidvm.ui.vm.boot; +import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; import static android.view.View.GONE; import static android.view.View.VISIBLE; import static cn.classfun.droidvm.lib.store.enums.Enums.optEnum; @@ -251,7 +252,7 @@ private static void updateSummary( summary.setVisibility(GONE); return; } - var line = entry.kernel + " | " + entry.source; + var line = fmt("%s | %s", entry.kernel, entry.source); var cmdline = entry.effectiveCmdline(boot.isVdafix()); if (!cmdline.isEmpty()) line += "\n" + cmdline; summary.setVisibility(VISIBLE); diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/display/DisplayProvider.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/display/DisplayProvider.java index f58252a6c..524e5db1b 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/display/DisplayProvider.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/display/DisplayProvider.java @@ -1,5 +1,6 @@ package cn.classfun.droidvm.ui.vm.display.nativedisplay.display; +import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; import android.crosvm.DisplayConfig; import android.crosvm.ICrosvmAndroidDisplayService; import android.os.DeadObjectException; @@ -105,7 +106,7 @@ private void fetchBinder() { if (got == null) { // Give up rather than poll forever: the daemon broker may have died (the supplier // returns null once rootService is dropped), so the display can't be reached. - Log.e(TAG, "display binder unavailable after " + MAX_BINDER_ATTEMPTS + " attempts"); + Log.e(TAG, fmt("display binder unavailable after %d attempts", MAX_BINDER_ATTEMPTS)); mainHandler.post(() -> onConnected.accept(false)); return; } @@ -130,7 +131,7 @@ private void onBinderReady(@NonNull IBinder binder) { onDisplayConfig.accept(config); } } catch (Exception e) { - Log.w(TAG, "getDisplayConfig unavailable, using default " + width + "x" + height); + Log.w(TAG, fmt("getDisplayConfig unavailable, using default %dx%d", width, height)); } onConnected.accept(true); applyPendingSurface(); diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/InputForwarder.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/InputForwarder.java index 163981464..ce192c4a7 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/InputForwarder.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/InputForwarder.java @@ -68,11 +68,11 @@ private void submit(@NonNull String name, @NonNull Runnable block) { try { block.run(); } catch (Exception e) { - Log.e(TAG, name + " failed", e); + Log.e(TAG, fmt("%s failed", name), e); } }); } catch (Exception e) { - Log.d(TAG, name + " dropped: " + e.getMessage()); + Log.d(TAG, fmt("%s dropped: %s", name, e.getMessage())); } } diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/info/VMInfoActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/info/VMInfoActivity.java index fac636af7..c5b822933 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/info/VMInfoActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/info/VMInfoActivity.java @@ -1,5 +1,6 @@ package cn.classfun.droidvm.ui.vm.info; +import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; import static android.widget.Toast.LENGTH_LONG; import static android.widget.Toast.LENGTH_SHORT; import static cn.classfun.droidvm.lib.store.enums.Enums.applyText; @@ -229,7 +230,7 @@ private String bootSummary() { var pinned = boot.getImageEntry(); var label = getString(R.string.edit_vm_kernel_source_image); if (pinned != null && pinned.title != null) - return label + " | " + pinned.title; + return fmt("%s | %s", label, pinned.title); return label; } return basename(boot.getKernel()); @@ -368,8 +369,7 @@ private void refreshPortForwards() { } catch (Exception e) { guest = "?"; } - lines.add(fwd.proto.toUpperCase() + " *:" + fwd.host - + " -> " + guest + ":" + fwd.guest); + lines.add(fmt("%s *:%s -> %s:%s", fwd.proto.toUpperCase(), fwd.host, guest, fwd.guest)); } } if (nic.isDhcp6LeaseEnabled() && vlan.isDhcp6Enabled()) { @@ -383,8 +383,7 @@ private void refreshPortForwards() { } catch (Exception e) { guest = "?"; } - lines.add(fwd.proto.toUpperCase() + " *:" + fwd.host - + " -> [" + guest + "]:" + fwd.guest); + lines.add(fmt("%s *:%s -> [%s]:%s", fwd.proto.toUpperCase(), fwd.host, guest, fwd.guest)); } } }); diff --git a/scripts/ci/check_string_concat.py b/scripts/ci/check_string_concat.py new file mode 100644 index 000000000..e6354feef --- /dev/null +++ b/scripts/ci/check_string_concat.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 +"""Flag Java string building done with `+` instead of the project's fmt() helper. + +Reports every `+` where exactly one operand is a string literal and the other is +a runtime expression (`"x=" + v`, `v + " done"`), which reads better as +`fmt("x=%s", v)`. Deliberately NOT flagged: + - `+` inside a string literal ("wlan+", "%s+", "\\s+") -- masked out first; + - literal + literal ("a" + "b") -- a compile-time constant, fmt adds nothing; + - `++` and numeric `+` with no string operand; + - a `+=` append with a SINGLE `+` (`s += "x" + y`) -- appending one piece to + an accumulator is clear intent fmt() would only obscure. But a `+=` chaining + two or more `+` (`s += "a" + b + "c"`) IS flagged: keep append complexity + low and reach for fmt() there. The count is per statement (to the next + `;`/`{`/`}`). + +A line is exempted with a trailing `// concat-ok` comment (for the rare case +where `+` genuinely reads better, e.g. a one-off constant split for width). + +Not a full parser: it masks comments and string/char literals, then inspects +the token on each side of every bare `+`. Good enough to catch the pattern the +reviewers keep flagging without drowning in false positives. + +Exit status is 0 when clean, 1 when any violation is found. Run from the repo +root, or pass the repo root as argv[1]. +""" +import os +import re +import subprocess +import sys + +SUPPRESS = "concat-ok" + + +def mask(src): + """Replace comments with spaces and each string/char literal with one STR + sentinel (\x01), preserving newlines so line numbers stay accurate.""" + out = [] + i, n = 0, len(src) + STR = "\x01" + while i < n: + c = src[i] + two = src[i:i + 2] + if two == "//": + j = src.find("\n", i) + j = n if j < 0 else j + out.append(" " * (j - i)) + i = j + elif two == "/*": + j = src.find("*/", i + 2) + j = n if j < 0 else j + 2 + out.append("".join(ch if ch == "\n" else " " for ch in src[i:j])) + i = j + elif c in "\"'": + j = i + 1 # consume literal, honour escapes + while j < n: + if src[j] == "\\": + j += 2 + continue + if src[j] == c: + j += 1 + break + j += 1 + out.append(STR) + i = j + else: + out.append(c) + i += 1 + return "".join(out) + + +# Significant tokens: a string literal (\x01), '+=' (marks an append statement), +# a value (identifier / number / closing bracket = end of a sub-expression), a +# '+', or a statement boundary (; { }) that scopes the '+=' exemption. +TOKEN = re.compile(r""" + \x01 # string literal + | \+\+ | \+= # compound operators (+= = append) + | \+ # the concatenation operator + | [A-Za-z_$][A-Za-z0-9_$]* | [0-9][\w.]* # identifier / number + | [)\]] # end of a sub-expression + | [;{}] # statement boundary +""", re.VERBOSE) + + +def _statement_hits(stmt, masked, suppressed): + """Violations within one statement's tokens. A '+=' append stays exempt only + while it is glance-obvious - at most ONE '+'. A second '+' (`s += "a"+b+"c"`) + is too complex: the exemption drops and every literal-joining '+' is reported, + steering it to fmt().""" + is_append = any(t == "+=" for t, _ in stmt) + plus_count = sum(1 for t, _ in stmt if t == "+") + if is_append and plus_count <= 1: # simple accumulator append: allow + return + for k, (tok, pos) in enumerate(stmt): + if tok != "+": + continue + left = stmt[k - 1][0] if k > 0 else None + right = stmt[k + 1][0] if k + 1 < len(stmt) else None + if (left == "\x01") != (right == "\x01"): # exactly one is a literal + line = masked.count("\n", 0, pos) + 1 + if line not in suppressed: + yield line + + +def violations_in(src, masked): + """Yield line numbers where a bare '+' joins a string literal and an expr. + Tokens are split into statements on ; { } so a '+=' append exemption cannot + leak past its statement and its '+' count is scoped correctly. Literal+literal + that spans lines stays within one statement, so it is still never flagged.""" + # suppression: any line carrying the marker is skipped. Checked against the + # original source, since masking blanks the comment the marker lives in. + suppressed = {i for i, ln in enumerate(src.split("\n"), 1) if SUPPRESS in ln} + stmt = [] + for m in TOKEN.finditer(masked): + tok = m.group(0) + if tok in (";", "{", "}"): + yield from _statement_hits(stmt, masked, suppressed) + stmt = [] + else: + stmt.append((tok, m.start())) + yield from _statement_hits(stmt, masked, suppressed) + + +def tracked_java(root): + out = subprocess.check_output(["git", "-C", root, "ls-files", "*.java"]) + return out.decode().split() + + +def main(): + root = os.path.abspath(sys.argv[1]) if len(sys.argv) > 1 else os.getcwd() + total = 0 + files = 0 + for rel in tracked_java(root): + p = os.path.join(root, rel) + if not os.path.isfile(p): + continue + with open(p, encoding="utf-8", errors="replace") as fh: + src = fh.read() + lines = sorted(set(violations_in(src, mask(src)))) + if not lines: + continue + files += 1 + total += len(lines) + raw = src.split("\n") + for ln in lines: + print(f"{rel}:{ln}: {raw[ln - 1].strip()[:100]}") + + if total: + print() + print(f"FAIL: {total} string concatenation(s) in {files} file(s); " + f"use fmt() instead of +.") + print(f"For a deliberate exception, add a trailing '// {SUPPRESS}' comment.") + return 1 + print("OK: no string-literal '+' concatenation.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From d3cdc05aa46c07028541d730ceb228562478a17b Mon Sep 17 00:00:00 2001 From: HuJK Date: Mon, 6 Jul 2026 16:55:31 +0800 Subject: [PATCH 093/182] acquire btn update --- .../ui/hugepage/AcquireContainerView.java | 171 ++++++++++++++++++ .../droidvm/ui/hugepage/HugePageActivity.java | 69 +++++-- .../ui/hugepage/HugePageProcessActivity.java | 6 + .../ui/hugepage/HugePageProcessAdapter.java | 5 +- app/src/main/res/layout/acquire_buttons.xml | 75 ++------ app/src/main/res/values-zh-rCN/strings.xml | 4 +- app/src/main/res/values-zh-rTW/strings.xml | 4 +- app/src/main/res/values/attrs.xml | 5 + app/src/main/res/values/strings.xml | 4 +- 9 files changed, 259 insertions(+), 84 deletions(-) create mode 100644 app/src/main/java/cn/classfun/droidvm/ui/hugepage/AcquireContainerView.java diff --git a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/AcquireContainerView.java b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/AcquireContainerView.java new file mode 100644 index 000000000..1a56bf0a5 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/AcquireContainerView.java @@ -0,0 +1,171 @@ +package cn.classfun.droidvm.ui.hugepage; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.content.res.AppCompatResources; +import androidx.core.graphics.ColorUtils; + +import com.google.android.material.color.MaterialColors; + +import cn.classfun.droidvm.R; + +/** + * The acquire-mode glyph drawn as a liquid-fill tank: an open-top box (no lid) + * with the download icon centred inside. The box fills with "water" up to a + * per-mode level - v1 shallow, v2 mid, v3 near-full - so the fill height, not a + * number badge, is what tells the three modes apart. + * + *

    The download glyph is split at the water line and drawn in opposite colours + * either side of it: {@code colorPrimary} above the surface (on the empty tank), + * {@code colorOnPrimary} for the submerged part (carved out of the water). The + * fill level is set per slot via {@code app:acv_fill} in {@code acquire_buttons.xml}. + * Greying/hiding is done by the host toggling the parent slot's alpha/visibility, + * so this view just draws itself at full strength. + */ +public final class AcquireContainerView extends View { + private final float density; + private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Path walls = new Path(); // open-top outline (stroked) + private final Path interior = new Path(); // rounded-bottom fill region (clip) + private final RectF box = new RectF(); + @Nullable + private final Drawable glyph; + + private final int colorOutline; + private final int colorWater; + private final int colorInterior; + private final int colorGlyphAbove; + private final int colorGlyphBelow; + + /** 0 = empty tank, 1 = full. Sets how high the water rises inside the box. */ + private float fill = 0.5f; + + public AcquireContainerView(Context context) { + this(context, null); + } + + public AcquireContainerView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + density = getResources().getDisplayMetrics().density; + Drawable d = AppCompatResources.getDrawable(context, R.drawable.ic_download); + glyph = d != null ? d.mutate() : null; // mutate: per-instance tint + int primary = MaterialColors.getColor(context, + androidx.appcompat.R.attr.colorPrimary, Color.BLUE); + int onPrimary = MaterialColors.getColor(context, + com.google.android.material.R.attr.colorOnPrimary, Color.WHITE); + colorOutline = primary; + colorWater = primary; + colorInterior = ColorUtils.setAlphaComponent(primary, 0x24); // faint tank tint + colorGlyphAbove = primary; // over the empty tank + colorGlyphBelow = onPrimary; // the opposite colour, under water + if (attrs != null) { + TypedArray a = context.obtainStyledAttributes( + attrs, R.styleable.AcquireContainerView); + fill = clamp01(a.getFloat(R.styleable.AcquireContainerView_acv_fill, fill)); + a.recycle(); + } + } + + /** Set the fill level (0..1) at runtime; clamped. */ + public void setFill(float value) { + float v = clamp01(value); + if (v != fill) { + fill = v; + invalidate(); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int def = Math.round(44 * density); + setMeasuredDimension(resolveSize(def, widthMeasureSpec), + resolveSize(def, heightMeasureSpec)); + } + + @Override + protected void onDraw(@NonNull Canvas canvas) { + super.onDraw(canvas); + float w = getWidth(), h = getHeight(); + if (w <= 0 || h <= 0) return; + + float side = Math.min(w, h) * 0.62f; // square tank, centred + float left = (w - side) / 2f; + float top = (h - side) / 2f; + float right = left + side; + float bottom = top + side; + box.set(left, top, right, bottom); + float radius = side * 0.18f; // rounded bottom corners + float stroke = Math.max(1.5f * density, side * 0.07f); + float inset = stroke / 2f; // keep the stroke inside the box + + // Water surface, measured from the bottom up. Clamp inside the tank. + float waterY = Math.max(top, Math.min(bottom, bottom - fill * side)); + + // Interior region (square top, rounded bottom) clips the tint fills so + // the water follows the rounded corners. + interior.reset(); + interior.addRoundRect(box, + new float[]{0, 0, 0, 0, radius, radius, radius, radius}, + Path.Direction.CW); + + canvas.save(); + canvas.clipPath(interior); + paint.setStyle(Paint.Style.FILL); + paint.setColor(colorInterior); // faint tint over the whole tank + canvas.drawRect(left, top, right, bottom, paint); + paint.setColor(colorWater); // opaque water in the lower part + canvas.drawRect(left, waterY, right, bottom, paint); + canvas.restore(); + + // Open-top walls: left side, rounded bottom, right side - no top edge. + walls.reset(); + walls.moveTo(left + inset, top); + walls.lineTo(left + inset, bottom - radius); + walls.quadTo(left + inset, bottom - inset, left + radius, bottom - inset); + walls.lineTo(right - radius, bottom - inset); + walls.quadTo(right - inset, bottom - inset, right - inset, bottom - radius); + walls.lineTo(right - inset, top); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(stroke); + paint.setStrokeCap(Paint.Cap.ROUND); + paint.setStrokeJoin(Paint.Join.ROUND); + paint.setColor(colorOutline); + canvas.drawPath(walls, paint); + + // Download glyph, centred and split at the water line: primary above the + // surface, the inverted on-primary colour carved out of the water below. + if (glyph != null) { + float g = side * 0.60f; + int gl = Math.round(left + (side - g) / 2f); + int gt = Math.round(top + (side - g) / 2f); + glyph.setBounds(gl, gt, Math.round(gl + g), Math.round(gt + g)); + + canvas.save(); + canvas.clipRect(left, top, right, waterY); + glyph.setTint(colorGlyphAbove); + glyph.draw(canvas); + canvas.restore(); + + canvas.save(); + canvas.clipRect(left, waterY, right, bottom); + glyph.setTint(colorGlyphBelow); + glyph.draw(canvas); + canvas.restore(); + } + } + + private static float clamp01(float v) { + return v < 0f ? 0f : Math.min(v, 1f); + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageActivity.java index bcd32a974..c859a6c1f 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageActivity.java @@ -21,6 +21,7 @@ import android.view.View; import android.util.Log; import android.view.inputmethod.EditorInfo; +import android.widget.FrameLayout; import android.widget.TextView; import android.widget.Toast; @@ -32,6 +33,7 @@ import com.google.android.material.appbar.MaterialToolbar; import com.google.android.material.button.MaterialButton; import com.google.android.material.card.MaterialCardView; +import com.google.android.material.checkbox.MaterialCheckBox; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.progressindicator.LinearProgressIndicator; @@ -57,6 +59,10 @@ public final class HugePageActivity extends AppCompatActivity { private static final String DISABLE_FILE = pathJoin(MAGISK_BASE, "disable"); private static final String CRASH_FILE = pathJoin(MAGISK_BASE, "crash"); private static final long PAGE_SIZE = 2L * 1024 * 1024; // 2MiB per page + // Shared app prefs (same store as Privacy/ApiManager) + the "don't ask again" + // flag for the acquire confirm dialog. + private static final String PREFS_NAME = "droidvm_prefs"; + private static final String KEY_SKIP_ACQUIRE_CONFIRM = "hugepage_acquire_skip_confirm"; private final Handler handler = new Handler(Looper.getMainLooper()); private final HugePageModel model = new HugePageModel(); private boolean resumed = false; @@ -164,12 +170,13 @@ private void initialize() { // while a run is in flight) interrupts it. Listeners are static -- the // slots aren't recycled -- and the idle/running visibility toggle is // driven by applyAcquireState(). - // Short-press runs it (gated to the pressable state); long-press opens the - // what-does-this-do dialog with a Run/Cancel choice (always available, even - // when the button is greyed - the buttons stay enabled for that). - btnAcquireV1.setOnClickListener(v -> { if (acquireEnabled && !mainAcquiring) startAcquire(1); }); - btnAcquireV2.setOnClickListener(v -> { if (acquireEnabled && !mainAcquiring) startAcquire(2); }); - btnAcquireV3.setOnClickListener(v -> { if (acquireEnabled && !mainAcquiring) startAcquire(3); }); + // Short-press opens the what-does-this-do dialog (or, once the user ticks + // "don't ask again", runs straight away) - gated to the pressable state. + // Long-press always opens the dialog, even when the button is greyed (the + // buttons stay enabled for that), so the prompt can be re-summoned any time. + btnAcquireV1.setOnClickListener(v -> { if (acquireEnabled && !mainAcquiring) confirmAcquire(this, 1, () -> startAcquire(1)); }); + btnAcquireV2.setOnClickListener(v -> { if (acquireEnabled && !mainAcquiring) confirmAcquire(this, 2, () -> startAcquire(2)); }); + btnAcquireV3.setOnClickListener(v -> { if (acquireEnabled && !mainAcquiring) confirmAcquire(this, 3, () -> startAcquire(3)); }); btnAcquireV1.setOnLongClickListener(v -> { showAcquireInfo(this, 1, () -> startAcquire(1)); return true; }); btnAcquireV2.setOnLongClickListener(v -> { showAcquireInfo(this, 2, () -> startAcquire(2)); return true; }); btnAcquireV3.setOnLongClickListener(v -> { showAcquireInfo(this, 3, () -> startAcquire(3)); return true; }); @@ -205,26 +212,58 @@ private void applyAcquireSlot(View btn, View spinner, int mode) { spinner.setVisibility(spin ? VISIBLE : GONE); } - /** Long-press info: what the acquire mode does, with a Run/Cancel choice. */ + /** + * Short-press an acquire button: run straight away if the user ticked "don't + * ask again" in a past prompt, otherwise show the confirm dialog. Long-press + * bypasses this and always shows the dialog (see {@link #showAcquireInfo}). + */ + static void confirmAcquire(@NonNull Context ctx, int mode, @NonNull Runnable onRun) { + if (skipAcquireConfirm(ctx)) onRun.run(); + else showAcquireInfo(ctx, mode, onRun); + } + + /** + * Explain what the acquire mode does, with a Run/Cancel choice and a "don't ask + * again" checkbox. The checkbox seeds from and (on any dismiss) writes back the + * skip preference, so it doubles as the way to re-enable the prompt after opting + * out; Run additionally starts the acquire. Shared by both hugepage screens, for + * short-press (via {@link #confirmAcquire}) and long-press alike. The title is + * always "Acquire huge pages" - the mode is conveyed by the explanation text. + */ static void showAcquireInfo(@NonNull Context ctx, int mode, @NonNull Runnable onRun) { - int modeLabel = mode == 2 ? R.string.hugepage_proc_acquire_v2 - : mode == 3 ? R.string.hugepage_proc_acquire_v3 - : R.string.hugepage_proc_acquire_v1; int msg = mode == 2 ? R.string.hugepage_acquire_v2_explain : mode == 3 ? R.string.hugepage_acquire_v3_explain : R.string.hugepage_acquire_v1_explain; - // Title = "Acquire huge pages" + the mode badge, e.g. "Acquire huge pages v1". - String title = fmt("%s %s", - ctx.getString(R.string.hugepage_acquire_pages_title), - ctx.getString(modeLabel)); + // "Don't ask again", indented to line up with the dialog's message text. + float density = ctx.getResources().getDisplayMetrics().density; + var dontAsk = new MaterialCheckBox(ctx); + dontAsk.setText(R.string.hugepage_acquire_dont_ask); + dontAsk.setChecked(skipAcquireConfirm(ctx)); + var holder = new FrameLayout(ctx); + int padH = Math.round(24 * density); + holder.setPaddingRelative(padH, Math.round(4 * density), padH, 0); + holder.addView(dontAsk); new MaterialAlertDialogBuilder(ctx) - .setTitle(title) + .setTitle(R.string.hugepage_acquire_pages_title) // no v1/v2/v3 suffix .setMessage(msg) + .setView(holder) .setPositiveButton(R.string.hugepage_acquire_run, (d, w) -> onRun.run()) .setNegativeButton(android.R.string.cancel, null) + .setOnDismissListener(d -> setSkipAcquireConfirm(ctx, dontAsk.isChecked())) .show(); } + /** True once the user opted out of the acquire prompt (short-press runs directly). */ + static boolean skipAcquireConfirm(@NonNull Context ctx) { + return ctx.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + .getBoolean(KEY_SKIP_ACQUIRE_CONFIRM, false); + } + + private static void setSkipAcquireConfirm(@NonNull Context ctx, boolean skip) { + ctx.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + .edit().putBoolean(KEY_SKIP_ACQUIRE_CONFIRM, skip).apply(); + } + /** * Start filling the pool with acquire algorithm {@code mode} (see * {@link HugePageModel#acquire}). Optimistically shows the spinners, fires the diff --git a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcessActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcessActivity.java index 1bdce4994..20a47990e 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcessActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcessActivity.java @@ -439,6 +439,12 @@ public void onAcquire(int mode) { }, "hugepage-acquire").start(); } + /** Short-press an acquire button: confirm (unless the user opted out), then run. */ + @Override + public void onAcquireConfirm(int mode) { + HugePageActivity.confirmAcquire(this, mode, () -> onAcquire(mode)); + } + /** Long-press an acquire button: explain the mode, with a Run/Cancel choice. */ @Override public void onAcquireInfo(int mode) { diff --git a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcessAdapter.java b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcessAdapter.java index 1f41f3fa9..39f138899 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcessAdapter.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcessAdapter.java @@ -36,6 +36,9 @@ public interface Listener { * 3 = migrate + per-block evict to zram. */ void onAcquire(int mode); + /** Short-press: confirm via the info dialog (unless the user opted out), then run. */ + void onAcquireConfirm(int mode); + /** Long-press: show the what-does-this-mode-do dialog (with a Run choice). */ void onAcquireInfo(int mode); @@ -183,7 +186,7 @@ private void bindAcquireSlot(View btn, View spinner, int mode) { // Left enabled so long-press always opens the info dialog; short-press is // gated on !acquiring and the icon greys via alpha while a run is in flight. btn.setAlpha(acquiring ? 0.38f : 1f); - btn.setOnClickListener(v -> { if (!acquiring) listener.onAcquire(mode); }); + btn.setOnClickListener(v -> { if (!acquiring) listener.onAcquireConfirm(mode); }); btn.setOnLongClickListener(v -> { listener.onAcquireInfo(mode); return true; }); spinner.setVisibility(spin ? VISIBLE : GONE); spinner.setOnClickListener(spin ? v -> listener.onStopAcquire() : null); diff --git a/app/src/main/res/layout/acquire_buttons.xml b/app/src/main/res/layout/acquire_buttons.xml index fb26d474c..61258510b 100644 --- a/app/src/main/res/layout/acquire_buttons.xml +++ b/app/src/main/res/layout/acquire_buttons.xml @@ -1,8 +1,10 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 64c71f4bf..16bb6b152 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -962,13 +962,11 @@ Failed to trigger acquire Save Acquire - v1 - v2 - v3 Acquire v1 (alloc_contig_pages scan) Acquire v2 (migrate + system reclaim) Acquire v3 (migrate + per-block evict to zram) Run + Don\'t ask again Uses alloc_contig_pages to collect huge pages Uses try_to_free_mem_cgroup_pages to push other apps into swap/zram\nthen alloc_contig_range to collect huge pages Uses folio_isolate_lru + reclaim_pages to precisely push pages into swap/zram\nthen alloc_contig_range to collect huge pages From 80e04c80bc58cf02afe864f5eb1823ebb8adf507 Mon Sep 17 00:00:00 2001 From: HuJK Date: Tue, 7 Jul 2026 02:47:16 +0800 Subject: [PATCH 094/182] hugepage: show acquire_stop_reason in the acquire-done bubble The gh_hugepage_reserve module now exports acquire_stop_reason in refill_stat (why the last user-triggered acquire stopped). Thread it through Snapshot as a pass-through display string (like state/totalServed) and append it to the acquire-done Toast, so the user sees the reason in the bubble instead of having to read dmesg. Kernel free text; appended raw (localization is a follow-up). Co-Authored-By: Claude Fable 5 --- .../droidvm/ui/hugepage/HugePageActivity.java | 11 ++++++++--- .../classfun/droidvm/ui/hugepage/HugePageModel.java | 10 +++++++--- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageActivity.java index c859a6c1f..f562b72de 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageActivity.java @@ -459,13 +459,18 @@ poolAvail, fmt("%s\n%s", getString(R.string.hugepage_bar_available), SizeUtils.f if (wasAcquiring && !acquiring) { long got = snap.free + snap.lent; long want = snap.targetIdeal; - Toast.makeText(this, got >= want + String msg = got >= want ? getString(R.string.hugepage_proc_acquire_full, SizeUtils.formatSize(want * PAGE_SIZE)) : getString(R.string.hugepage_proc_acquire_partial, SizeUtils.formatSize(got * PAGE_SIZE), - SizeUtils.formatSize(want * PAGE_SIZE)), - Toast.LENGTH_LONG).show(); + SizeUtils.formatSize(want * PAGE_SIZE)); + // Append why the acquire stopped (kernel free text from refill_stat's + // acquire_stop_reason), so the user sees the reason in the bubble. + String reason = snap.acquireStopReason; + if (!reason.isEmpty() && !"-".equals(reason)) + msg += "\n" + reason; + Toast.makeText(this, msg, Toast.LENGTH_LONG).show(); } wasAcquiring = acquiring; if (mainAcquiring != acquiring || mainAcquireMode != mode) { diff --git a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageModel.java b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageModel.java index f26d7e977..8d9a92e39 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageModel.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageModel.java @@ -182,12 +182,14 @@ static final class Snapshot { @NonNull final String totalServed; @NonNull final String totalRefilled; @NonNull final String activeVms; + @NonNull final String acquireStopReason; // why the last acquire stopped ("-" if unreported) private Snapshot(boolean installed, boolean loaded, boolean statsOk, boolean bootEnabled, long targetIdeal, long built, long free, long lent, long deficit, boolean acquiring, int acquireMode, boolean hasPoolWant, boolean softDisabled, @NonNull String state, @NonNull String totalServed, - @NonNull String totalRefilled, @NonNull String activeVms) { + @NonNull String totalRefilled, @NonNull String activeVms, + @NonNull String acquireStopReason) { this.installed = installed; this.loaded = loaded; this.statsOk = statsOk; @@ -205,6 +207,7 @@ private Snapshot(boolean installed, boolean loaded, boolean statsOk, boolean boo this.totalServed = totalServed; this.totalRefilled = totalRefilled; this.activeVms = activeVms; + this.acquireStopReason = acquireStopReason; } } @@ -223,7 +226,7 @@ Snapshot state() { boolean loaded = existsSticky(SYSFS_BASE); if (!loaded) { return new Snapshot(installed, false, false, bootEnabled, - 0, 0, 0, 0, 0, false, -1, false, false, "-", "-", "-", "-"); + 0, 0, 0, 0, 0, false, -1, false, false, "-", "-", "-", "-", "-"); } var s = parseProp(safeRead(pathJoin(SYSFS_PARAMS, "refill_stat"))); boolean statsOk = !s.isEmpty(); // empty == the read failed transiently @@ -242,7 +245,8 @@ Snapshot state() { return new Snapshot(installed, true, statsOk, bootEnabled, want, built, free, lent, deficit, acquiring, mode, hasWant, softDisabled, s.getOrDefault("state", "-"), s.getOrDefault("total_served", "-"), - s.getOrDefault("total_refilled", "-"), s.getOrDefault("active_vms", "-")); + s.getOrDefault("total_refilled", "-"), s.getOrDefault("active_vms", "-"), + s.getOrDefault("acquire_stop_reason", "-")); } /** From f8021e1e067bc508c0c0f1f00b6c5b4b6554dce1 Mon Sep 17 00:00:00 2001 From: BigfootACA Date: Fri, 3 Jul 2026 20:01:09 +0800 Subject: [PATCH 095/182] daemon: add SetAppConfig Signed-off-by: BigfootACA --- .../daemon/ipc/basic/SetAppConfig.java | 29 +++++++++++++++++++ .../droidvm/daemon/server/ServerContext.java | 2 ++ .../droidvm/daemon/vm/VMBackendInstance.java | 5 +++- .../droidvm/daemon/vm/VMInstance.java | 2 +- .../daemon/vm/backend/BackendBase.java | 6 +++- .../daemon/vm/backend/CrosvmBackend.java | 8 +++-- .../vm/backend/CrosvmBackendInstance.java | 8 +++-- .../daemon/vm/backend/QemuBackend.java | 8 +++-- .../vm/backend/QemuBackendInstance.java | 8 +++-- .../droidvm/lib/daemon/VMEventHandler.java | 26 +++++++++++++++++ .../main/settings/MainSettingsFragment.java | 7 +++-- 11 files changed, 96 insertions(+), 13 deletions(-) create mode 100644 app/src/main/java/cn/classfun/droidvm/daemon/ipc/basic/SetAppConfig.java diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/basic/SetAppConfig.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/basic/SetAppConfig.java new file mode 100644 index 000000000..33db76b3b --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/basic/SetAppConfig.java @@ -0,0 +1,29 @@ +package cn.classfun.droidvm.daemon.ipc.basic; + +import androidx.annotation.NonNull; + +import com.google.auto.service.AutoService; + +import cn.classfun.droidvm.daemon.server.ClientRequest; +import cn.classfun.droidvm.daemon.server.RequestHandler; +import cn.classfun.droidvm.lib.store.base.DataItem; + +@AutoService(RequestHandler.class) +public class SetAppConfig extends RequestHandler { + @NonNull + @Override + public String getName() { + return "set_app_config"; + } + + @Override + public void handle(@NonNull ClientRequest request) throws Exception { + var params = request.getParams(); + if (!params.has("config")) + throw new RuntimeException("missing config"); + var cfg = params.getJSONObject("config"); + var data = DataItem.fromJson(cfg); + var ctx = request.getContext(); + ctx.appConfig = data; + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/server/ServerContext.java b/app/src/main/java/cn/classfun/droidvm/daemon/server/ServerContext.java index 204573912..043c2cb60 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/server/ServerContext.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/server/ServerContext.java @@ -15,12 +15,14 @@ import cn.classfun.droidvm.daemon.network.NetworkInstanceStore; import cn.classfun.droidvm.daemon.network.backend.DefaultRouterWatcher; import cn.classfun.droidvm.daemon.vm.VMInstanceStore; +import cn.classfun.droidvm.lib.store.base.DataItem; public final class ServerContext { private static final String TAG = "ServerContext"; private final VMInstanceStore vms = new VMInstanceStore(this); private final NetworkInstanceStore networks = new NetworkInstanceStore(this); private final DefaultRouterWatcher routerWatcher = new DefaultRouterWatcher(this); + public DataItem appConfig = DataItem.newObject(); public ServerContext() { Log.i(TAG, "loading config files..."); diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMBackendInstance.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMBackendInstance.java index 86d5ea32b..9b8464893 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMBackendInstance.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMBackendInstance.java @@ -18,6 +18,7 @@ import java.util.Map; import cn.classfun.droidvm.daemon.console.ConsoleStream; +import cn.classfun.droidvm.daemon.server.ServerContext; import cn.classfun.droidvm.lib.natives.NativeProcess; import cn.classfun.droidvm.lib.store.vm.VMConfig; import cn.classfun.droidvm.lib.utils.FileUtils; @@ -26,9 +27,11 @@ public abstract class VMBackendInstance { private static final String TAG = "VMBackendInstance"; protected final Map streams = new LinkedHashMap<>(); protected final VMConfig config; + protected final ServerContext context; private boolean stracePrepared = false; - protected VMBackendInstance(@NonNull VMConfig config) { + protected VMBackendInstance(@NonNull ServerContext context, @NonNull VMConfig config) { + this.context = context; this.config = config; } diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMInstance.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMInstance.java index c69030ff5..9da40e025 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMInstance.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMInstance.java @@ -84,7 +84,7 @@ public BackendBase getBackend() { @NonNull synchronized VMBackendInstance getBackendInstance() { if (this.backendInstance == null) - this.backendInstance = getBackend().create(this); + this.backendInstance = getBackend().create(store.context, this); return this.backendInstance; } diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/BackendBase.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/BackendBase.java index 23e30270b..41de65647 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/BackendBase.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/BackendBase.java @@ -11,6 +11,7 @@ import java.util.Map; import java.util.ServiceLoader; +import cn.classfun.droidvm.daemon.server.ServerContext; import cn.classfun.droidvm.daemon.vm.VMBackendInstance; import cn.classfun.droidvm.lib.store.vm.VMConfig; @@ -23,7 +24,10 @@ public abstract class BackendBase { public abstract String name(); @NonNull - public abstract VMBackendInstance create(@NonNull VMConfig config); + public abstract VMBackendInstance create( + @NonNull ServerContext context, + @NonNull VMConfig config + ); public static void loadAll() { if (!backends.isEmpty()) return; diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackend.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackend.java index 2444db096..37185f3c0 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackend.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackend.java @@ -4,6 +4,7 @@ import com.google.auto.service.AutoService; +import cn.classfun.droidvm.daemon.server.ServerContext; import cn.classfun.droidvm.daemon.vm.VMBackendInstance; import cn.classfun.droidvm.lib.store.vm.VMConfig; @@ -17,7 +18,10 @@ public String name() { @NonNull @Override - public VMBackendInstance create(@NonNull VMConfig config) { - return new CrosvmBackendInstance(config); + public VMBackendInstance create( + @NonNull ServerContext context, + @NonNull VMConfig config + ) { + return new CrosvmBackendInstance(context, config); } } diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackendInstance.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackendInstance.java index f83505f23..1e1ef05cd 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackendInstance.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackendInstance.java @@ -28,6 +28,7 @@ import cn.classfun.droidvm.daemon.console.FDPipeConsoleStream; import cn.classfun.droidvm.daemon.console.InputConsoleStream; import cn.classfun.droidvm.daemon.console.SimpleConsoleStream; +import cn.classfun.droidvm.daemon.server.ServerContext; import cn.classfun.droidvm.daemon.vm.BootPlan; import cn.classfun.droidvm.daemon.vm.SerialPipe; import cn.classfun.droidvm.daemon.vm.VMBackendInstance; @@ -60,8 +61,11 @@ public final class CrosvmBackendInstance extends VMBackendInstance { private final InputConsoleStream stderrStream; private final SimpleConsoleStream stdioStream; - public CrosvmBackendInstance(@NonNull VMConfig config) { - super(config); + public CrosvmBackendInstance( + @NonNull ServerContext context, + @NonNull VMConfig config + ) { + super(context, config); uartStream = new FDPipeConsoleStream(config, "uart", -1, -1); stdoutStream = new InputConsoleStream(config, "stdout", null); stderrStream = new InputConsoleStream(config, "stderr", null); diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/QemuBackend.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/QemuBackend.java index cf32288f0..6479786d8 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/QemuBackend.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/QemuBackend.java @@ -4,6 +4,7 @@ import com.google.auto.service.AutoService; +import cn.classfun.droidvm.daemon.server.ServerContext; import cn.classfun.droidvm.daemon.vm.VMBackendInstance; import cn.classfun.droidvm.lib.store.vm.VMConfig; @@ -17,7 +18,10 @@ public String name() { @NonNull @Override - public VMBackendInstance create(@NonNull VMConfig config) { - return new QemuBackendInstance(config); + public VMBackendInstance create( + @NonNull ServerContext context, + @NonNull VMConfig config + ) { + return new QemuBackendInstance(context, config); } } diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/QemuBackendInstance.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/QemuBackendInstance.java index 3897aa929..dcf322f8e 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/QemuBackendInstance.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/QemuBackendInstance.java @@ -29,6 +29,7 @@ import cn.classfun.droidvm.daemon.console.InputConsoleStream; import cn.classfun.droidvm.daemon.console.LocalSocketConsoleStream; import cn.classfun.droidvm.daemon.console.SimpleConsoleStream; +import cn.classfun.droidvm.daemon.server.ServerContext; import cn.classfun.droidvm.daemon.vm.BootPlan; import cn.classfun.droidvm.daemon.vm.VMBackendInstance; import cn.classfun.droidvm.daemon.vm.VMStartResult; @@ -56,8 +57,11 @@ public final class QemuBackendInstance extends VMBackendInstance { private final InputConsoleStream stderrStream; private final SimpleConsoleStream stdioStream; - public QemuBackendInstance(@NonNull VMConfig config) { - super(config); + public QemuBackendInstance( + @NonNull ServerContext context, + @NonNull VMConfig config + ) { + super(context, config); uartStream = new LocalSocketConsoleStream(config, "uart", null); stdoutStream = new InputConsoleStream(config, "stdout", null); stderrStream = new InputConsoleStream(config, "stderr", null); diff --git a/app/src/main/java/cn/classfun/droidvm/lib/daemon/VMEventHandler.java b/app/src/main/java/cn/classfun/droidvm/lib/daemon/VMEventHandler.java index 26788ee84..da8923c46 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/daemon/VMEventHandler.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/daemon/VMEventHandler.java @@ -1,5 +1,6 @@ package cn.classfun.droidvm.lib.daemon; +import static android.content.Context.MODE_PRIVATE; import static android.widget.Toast.LENGTH_LONG; import static android.widget.Toast.LENGTH_SHORT; import static cn.classfun.droidvm.lib.utils.FileUtils.calcHashForFile; @@ -41,6 +42,7 @@ import cn.classfun.droidvm.lib.store.vm.VMState; import cn.classfun.droidvm.lib.store.vm.VMStore; import cn.classfun.droidvm.lib.ui.UIContext; +import cn.classfun.droidvm.lib.utils.ThreadUtils; import cn.classfun.droidvm.ui.SplashActivity; import cn.classfun.droidvm.ui.main.MainActivity; import cn.classfun.droidvm.ui.vm.console.VMConsoleActivity; @@ -203,6 +205,7 @@ public void onDaemonConnected() { queueActivityTask(act -> Toast.makeText( act, R.string.daemon_connected, Toast.LENGTH_SHORT ).show()); + queueActivityTask(VMEventHandler::sendAppConfig); } @Override @@ -214,6 +217,29 @@ public void onDaemonDisconnected() { ).show()); } + public static void sendAppConfig(Activity act) { + var req = new JSONObject(); + try { + var pref = act.getSharedPreferences("droidvm_prefs", MODE_PRIVATE); + req.put("command", "set_app_config"); + req.put("config", new JSONObject(pref.getAll())); + } catch (Exception e) { + Log.w(TAG, "Failed to prepare app config", e); + } + ThreadUtils.runOnPool(() -> { + try { + var conn = DaemonConnection.getInstance(); + var resp = conn.request(req); + if (!resp.optBoolean("success", false)) { + var msg = resp.optString("message", "unknown error"); + Log.w(TAG, fmt("set_app_config failed: %s", msg)); + } + } catch (Exception e) { + Log.w(TAG, "Failed to send app config to daemon", e); + } + }); + } + private boolean isOutdatedDaemon(JSONObject resp) { try { var daemonHash = resp.optString("daemon_hash", ""); diff --git a/app/src/main/java/cn/classfun/droidvm/ui/main/settings/MainSettingsFragment.java b/app/src/main/java/cn/classfun/droidvm/ui/main/settings/MainSettingsFragment.java index 72f80ba7d..fe1b02ae2 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/main/settings/MainSettingsFragment.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/main/settings/MainSettingsFragment.java @@ -45,6 +45,7 @@ import cn.classfun.droidvm.lib.api.ApiManager; import cn.classfun.droidvm.lib.api.Privacy; import cn.classfun.droidvm.lib.daemon.DaemonHelper; +import cn.classfun.droidvm.lib.daemon.VMEventHandler; import cn.classfun.droidvm.lib.data.Language; import cn.classfun.droidvm.lib.store.base.DataConfig; import cn.classfun.droidvm.lib.store.base.DataStore; @@ -119,8 +120,10 @@ private void bindOnClick(@NonNull View view, Runnable action) { private void bindOnChecked(@NonNull SwitchRowWidget item, String key, boolean def) { var prefs = requireContext().getSharedPreferences(PREFS_NAME, 0); item.setChecked(prefs.getBoolean(key, def)); - item.setOnCheckedChangeListener((btn, checked) -> - prefs.edit().putBoolean(key, checked).apply()); + item.setOnCheckedChangeListener((btn, checked) -> { + prefs.edit().putBoolean(key, checked).apply(); + VMEventHandler.sendAppConfig(requireActivity()); + }); } @Override From 5944195bc285f2f187b3ac528b434f0e158f22f4 Mon Sep 17 00:00:00 2001 From: BigfootACA Date: Fri, 3 Jul 2026 20:02:27 +0800 Subject: [PATCH 096/182] MainSettingsFragment: add direct access to external storage Signed-off-by: BigfootACA --- .../classfun/droidvm/daemon/vm/VMBackendInstance.java | 10 ++++++++++ .../daemon/vm/backend/CrosvmBackendInstance.java | 1 + .../droidvm/daemon/vm/backend/QemuBackendInstance.java | 1 + .../droidvm/ui/main/settings/MainSettingsFragment.java | 4 ++++ app/src/main/res/drawable/ic_sd.xml | 9 +++++++++ app/src/main/res/layout/fragment_main_settings.xml | 8 ++++++++ app/src/main/res/values-zh-rCN/strings.xml | 2 ++ app/src/main/res/values-zh-rTW/strings.xml | 2 ++ app/src/main/res/values/strings.xml | 2 ++ 9 files changed, 39 insertions(+) create mode 100644 app/src/main/res/drawable/ic_sd.xml diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMBackendInstance.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMBackendInstance.java index 9b8464893..28f4b4ea9 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMBackendInstance.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMBackendInstance.java @@ -6,6 +6,7 @@ import static cn.classfun.droidvm.lib.utils.RunUtils.run; import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; import static cn.classfun.droidvm.lib.utils.StringUtils.pathJoin; +import static cn.classfun.droidvm.ui.main.settings.MainSettingsFragment.KEY_OPTIMIZE_SDCARD; import android.os.Build; import android.util.Log; @@ -115,4 +116,13 @@ protected void prepareStraceArguments(@NonNull List args) { args.add("-o"); args.add(pathJoin(dir, "strace.log")); } + + protected String patchOptimizedPath(@NonNull String path) { + var en = context.appConfig.optBoolean(KEY_OPTIMIZE_SDCARD, true); + var prefix = "/storage/emulated/"; + var newPrefix = "/data/media/"; + if (en && path.startsWith(prefix)) + path = newPrefix + path.substring(prefix.length()); + return path; + } } diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackendInstance.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackendInstance.java index 1e1ef05cd..f38b1320b 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackendInstance.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackendInstance.java @@ -243,6 +243,7 @@ private void buildDiskCommand(@NonNull List args) { var disk = iter.getValue(); var path = disk.optString("path", ""); if (path.isEmpty()) continue; + path = patchOptimizedPath(path); var readonly = disk.optBoolean("readonly", false); var bus = optEnum(disk, "bus", DiskBus.VIRTIO); var arg = new StringBuilder(path); diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/QemuBackendInstance.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/QemuBackendInstance.java index dcf322f8e..8cff0bcd5 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/QemuBackendInstance.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/QemuBackendInstance.java @@ -245,6 +245,7 @@ private void buildDiskCommand(@NonNull List args) { var disk = iter.getValue(); var path = disk.optString("path", ""); if (path.isEmpty()) continue; + path = patchOptimizedPath(path); var readonly = disk.optBoolean("readonly", false); var bus = optEnum(disk, "bus", DiskBus.VIRTIO); switch (bus) { diff --git a/app/src/main/java/cn/classfun/droidvm/ui/main/settings/MainSettingsFragment.java b/app/src/main/java/cn/classfun/droidvm/ui/main/settings/MainSettingsFragment.java index fe1b02ae2..4639280e5 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/main/settings/MainSettingsFragment.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/main/settings/MainSettingsFragment.java @@ -71,6 +71,7 @@ public final class MainSettingsFragment extends MainBaseFragment { public static final String KEY_VM_CLEAR_LOGS_BEFORE_START = "vm_clear_logs_before_start"; public static final String KEY_VM_KEEP_COMPRESS_ON_OPTIMIZE = "vm_keep_compress_on_optimize"; public static final String KEY_QEMU_IMG_CPU_AFFINITY = "qemu_img_cpu_affinity"; + public static final String KEY_OPTIMIZE_SDCARD = "optimize_sdcard"; public static final String KEY_AUTO_CHECK_UPDATE = "auto_check_update"; private final Handler mainHandler = new Handler(Looper.getMainLooper()); private final Runnable daemonStatusRefreshRunnable = this::periodRefreshDaemonStatus; @@ -85,6 +86,7 @@ public final class MainSettingsFragment extends MainBaseFragment { private SwitchRowWidget itemVMAutoConsole; private SwitchRowWidget itemVMClearLogsBeforeStart; private SwitchRowWidget itemVMKeepCompressOnOptimize; + private SwitchRowWidget itemOptimizeSdcard; private TextRowWidget itemCpuAffinity; private TextRowWidget itemLicense; private SwitchRowWidget itemAutoCheckUpdate; @@ -141,6 +143,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat itemVMAutoConsole = view.findViewById(R.id.item_vm_auto_console); itemVMClearLogsBeforeStart = view.findViewById(R.id.item_vm_clear_logs_before_start); itemVMKeepCompressOnOptimize = view.findViewById(R.id.item_vm_keep_compress_on_optimize); + itemOptimizeSdcard = view.findViewById(R.id.item_optimize_sdcard); itemCpuAffinity = view.findViewById(R.id.item_cpu_affinity); itemLicense = view.findViewById(R.id.item_license); itemAutoCheckUpdate = view.findViewById(R.id.item_auto_check_update); @@ -175,6 +178,7 @@ private void initSettings() { bindOnChecked(itemVMKeepCompressOnOptimize, KEY_VM_KEEP_COMPRESS_ON_OPTIMIZE, false); bindOnClick(itemCpuAffinity, this::showCpuAffinityDialog); refreshCpuAffinitySummary(); + bindOnChecked(itemOptimizeSdcard, KEY_OPTIMIZE_SDCARD, true); bindOnChecked(itemAutoCheckUpdate, KEY_AUTO_CHECK_UPDATE, true); bindOnClick(itemCheckUpdate, this::checkUpdate); bindOnClick(itemPrivacy, this::showPrivacyPolicy); diff --git a/app/src/main/res/drawable/ic_sd.xml b/app/src/main/res/drawable/ic_sd.xml new file mode 100644 index 000000000..690586dbd --- /dev/null +++ b/app/src/main/res/drawable/ic_sd.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/fragment_main_settings.xml b/app/src/main/res/layout/fragment_main_settings.xml index 5deeeb484..ed546d586 100644 --- a/app/src/main/res/layout/fragment_main_settings.xml +++ b/app/src/main/res/layout/fragment_main_settings.xml @@ -71,6 +71,14 @@ android:subtitle="@string/settings_vm_keep_compress_on_optimize_summary" android:text="@string/settings_vm_keep_compress_on_optimize_title" /> + + 小核 大核 超大核 + 直接访问外部存储 + 使用 /data/media 代替 /storage/emulated 以提升磁盘性能 关于 问题反馈 在 GitHub 上报告问题 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 152813e54..4733a634f 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -376,6 +376,8 @@ 小核 大核 超大核 + 直接存取外部存儲 + 使用 /data/media 取代 /storage/emulated 以提升磁碟效能 關於 問題反饋 在 GitHub 上報告問題 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 912740987..f87ab2185 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -378,6 +378,8 @@ little big prime + Direct access to external storage + Use /data/media instead of /storage/emulated to improve disk performance About Feedback Report issues on GitHub From a182a649859550f8219c438a6248096bd19f1354 Mon Sep 17 00:00:00 2001 From: BigfootACA Date: Sat, 4 Jul 2026 16:41:29 +0800 Subject: [PATCH 097/182] LogsActivity: move logcat to daemon Signed-off-by: BigfootACA --- .../cn/classfun/droidvm/daemon/Daemon.java | 47 +++++++++-- .../droidvm/ui/logs/LogsActivity.java | 80 +++++++++++++------ app/src/main/res/values-zh-rCN/strings.xml | 2 + app/src/main/res/values-zh-rTW/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + 5 files changed, 102 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/Daemon.java b/app/src/main/java/cn/classfun/droidvm/daemon/Daemon.java index 080d668dc..6db9c1229 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/Daemon.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/Daemon.java @@ -1,15 +1,18 @@ package cn.classfun.droidvm.daemon; +import static android.os.Process.myPid; import static cn.classfun.droidvm.BuildConfig.APPLICATION_ID; import static cn.classfun.droidvm.lib.Constants.DATA_DIR; import static cn.classfun.droidvm.lib.daemon.DaemonHelper.getPidFile; import static cn.classfun.droidvm.lib.daemon.DaemonHelper.getPortFile; import static cn.classfun.droidvm.lib.daemon.DaemonHelper.getTokenFile; +import static cn.classfun.droidvm.lib.utils.FileUtils.findExecute; +import static cn.classfun.droidvm.lib.utils.FileUtils.writeFile; import static cn.classfun.droidvm.lib.utils.RunUtils.runList; import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; import static cn.classfun.droidvm.lib.utils.StringUtils.pathJoin; -import android.os.Process; +import android.system.Os; import android.util.Log; import androidx.annotation.NonNull; @@ -34,10 +37,12 @@ public final class Daemon { public static final String TAG = "Daemon"; + public static final String LOG_PATH = pathJoin(DATA_DIR, "cache", "daemon.log"); private static final String RUN_DIR = pathJoin(DATA_DIR, "run"); private static final String LOCK_FILE = pathJoin(RUN_DIR, "droidvmd.lock"); private static final AtomicBoolean cleaned = new AtomicBoolean(false); public static String daemonHash = null; + private static Process logcatProcess = null; // Held for the whole process lifetime so the advisory lock stays taken; the // kernel drops it automatically when this process dies (including SIGKILL), @@ -127,7 +132,7 @@ private static void writeLockOwner() { try { lockChannel.truncate(0); lockChannel.position(0); - lockChannel.write(ByteBuffer.wrap(String.valueOf(Process.myPid()).getBytes())); + lockChannel.write(ByteBuffer.wrap(String.valueOf(myPid()).getBytes())); lockChannel.force(false); } catch (IOException e) { Log.w(TAG, "Failed to write lock owner pid", e); @@ -152,9 +157,9 @@ private static void writePidFile() { Log.w(TAG, fmt("Failed to create run directory: %s", runDir)); var pidFile = new File(runDir, "droidvmd.pid"); try (var writer = new FileWriter(pidFile)) { - writer.write(String.valueOf(Process.myPid())); + writer.write(String.valueOf(myPid())); writer.flush(); - Log.i(TAG, fmt("PID file written: %s (pid=%d)", pidFile, Process.myPid())); + Log.i(TAG, fmt("PID file written: %s (pid=%d)", pidFile, myPid())); } catch (IOException e) { Log.e(TAG, fmt("Failed to write PID file: %s", pidFile), e); } @@ -220,6 +225,35 @@ private static void setProcTitle() { } } + @SuppressWarnings("OctalInteger") + private static void startLogcat() { + try { + var builder = new ProcessBuilder(); + var file = new File(LOG_PATH); + if (file.exists() && !file.delete()) + Log.w(TAG, "failed to remove old log"); + writeFile(file, ""); + var uid = Server.getDroidVMUid(); + Os.chmod(LOG_PATH, 0600); + Os.chown(LOG_PATH, uid, uid); + builder.command( + findExecute("logcat", "/system/bin/logcat"), + fmt("--pid=%d", myPid()) + ); + builder.redirectOutput(new File(LOG_PATH)); + logcatProcess = builder.start(); + } catch (Exception e) { + Log.w(TAG, "failed to start logcat", e); + logcatProcess = null; + } + } + + private static void stopLogcat() { + if (logcatProcess == null) return; + logcatProcess.destroy(); + logcatProcess = null; + } + public static void main(String... args) { boolean force = Arrays.asList(args).contains("--force"); System.out.print("Starting DroidVM Daemon...\n"); @@ -228,7 +262,8 @@ public static void main(String... args) { // broadcast the native-display binder to the UI. Must happen before server.run() blocks here. DaemonSystemContext.init(); setProcTitle(); - System.out.printf("Current pid: %d\n", Process.myPid()); + startLogcat(); + System.out.printf("Current pid: %d\n", myPid()); Log.d(TAG, "DroidVM Daemon is starting..."); if (!acquireLock(force)) { System.out.print("Another DroidVM Daemon is already running.\n"); @@ -250,8 +285,10 @@ public static void main(String... args) { Runtime.getRuntime().addShutdownHook(new Thread(() -> { server.stop(); cleanup(server); + stopLogcat(); })); server.run(); cleanup(server); + stopLogcat(); } } diff --git a/app/src/main/java/cn/classfun/droidvm/ui/logs/LogsActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/logs/LogsActivity.java index 206cd9b76..0292a8946 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/logs/LogsActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/logs/LogsActivity.java @@ -1,6 +1,7 @@ package cn.classfun.droidvm.ui.logs; -import static cn.classfun.droidvm.lib.utils.FileUtils.findExecute; +import static java.nio.charset.StandardCharsets.UTF_8; +import static cn.classfun.droidvm.daemon.Daemon.LOG_PATH; import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; import android.net.Uri; @@ -18,11 +19,13 @@ import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.appbar.MaterialToolbar; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import java.io.BufferedReader; import java.io.BufferedWriter; -import java.io.InputStreamReader; +import java.io.ByteArrayOutputStream; +import java.io.File; import java.io.OutputStreamWriter; +import java.io.RandomAccessFile; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; @@ -31,7 +34,6 @@ import java.util.concurrent.atomic.AtomicBoolean; import cn.classfun.droidvm.R; -import cn.classfun.droidvm.lib.daemon.DaemonHelper; import cn.classfun.droidvm.lib.utils.ShareUtils; public final class LogsActivity extends AppCompatActivity { @@ -40,8 +42,9 @@ public final class LogsActivity extends AppCompatActivity { private RecyclerView recyclerView; private LogAdapter adapter; - private Process logcatProcess; + private RandomAccessFile logFile; private Thread readerThread; + private final ByteArrayOutputStream lineBuf = new ByteArrayOutputStream(8192); private volatile boolean autoScroll = true; private final List pendingLines = new ArrayList<>(); private final AtomicBoolean updated = new AtomicBoolean(false); @@ -80,7 +83,13 @@ public void onScrolled(@NonNull RecyclerView rv, int dx, int dy) { } } }); - startLogcat(); + if (new File(LOG_PATH).exists()) + startLogcat(); + else new MaterialAlertDialogBuilder(this) + .setTitle(R.string.logs_not_found_title) + .setMessage(R.string.logs_not_found_message) + .setPositiveButton(android.R.string.ok, null) + .show(); } private boolean onMenuItemClick(@NonNull MenuItem item) { @@ -157,13 +166,8 @@ private void startLogcat() { } updated.set(false); try { - var pid = DaemonHelper.readPid(); - if (pid < 0) throw new RuntimeException("Daemon is not running"); - var cmd = fmt("logcat --pid=%d", pid); - var shell = findExecute("su", "/system/bin/su"); - var builder = new ProcessBuilder(shell, "-c", cmd); - builder.redirectErrorStream(true); - logcatProcess = builder.start(); + logFile = new RandomAccessFile(LOG_PATH, "r"); + logFile.seek(0); } catch (Exception e) { var lines = new ArrayList(); lines.add(e.toString()); @@ -171,27 +175,48 @@ private void startLogcat() { return; } running = true; - readerThread = new Thread(this::logcatThread, "LogcatReader"); + lineBuf.reset(); + readerThread = new Thread(this::logcatThread, "LogReader"); readerThread.setDaemon(true); readerThread.start(); uiHandler.postDelayed(flushRunnable, FLUSH_INTERVAL_MS); } private void logcatThread() { - try ( - var input = new InputStreamReader(logcatProcess.getInputStream()); - var reader = new BufferedReader(input, 16384) - ) { - String line; - while (running && (line = reader.readLine()) != null) { - synchronized (pendingLines) { - pendingLines.add(line); - updated.set(true); + var buf = new byte[8192]; + try { + while (running) { + long len = logFile.length(); + long pos = logFile.getFilePointer(); + if (len < pos) { + logFile.seek(0); + pos = 0; + } + if (len > pos) { + int rl = (int) Math.min(buf.length, len - pos); + int n = logFile.read(buf, 0, rl); + if (n > 0) { + for (int i = 0; i < n; i++) { + byte b = buf[i]; + if (b == '\n') { + var line = lineBuf.toString(UTF_8); + lineBuf.reset(); + synchronized (pendingLines) { + pendingLines.add(line); + updated.set(true); + } + } else if (b != '\r') + lineBuf.write(b); + } + continue; + } } while (running && suspend.get()) { //noinspection BusyWait Thread.sleep(FLUSH_INTERVAL_MS); } + //noinspection BusyWait + Thread.sleep(FLUSH_INTERVAL_MS); } } catch (Exception ignored) { } @@ -220,9 +245,12 @@ private void flushBuffer() { private void stopLogcat() { running = false; uiHandler.removeCallbacks(flushRunnable); - if (logcatProcess != null) { - logcatProcess.destroy(); - logcatProcess = null; + if (logFile != null) { + try { + logFile.close(); + } catch (Exception ignored) { + } + logFile = null; } if (readerThread != null) { readerThread.interrupt(); diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 05f70cafa..90fd075f8 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -890,6 +890,8 @@ 分享 分享日志 分享日志失败:%1$s + 守护进程日志不可用 + 找不到守护进程的日志文件,守护进程是否正在运行? 隐私政策 继续之前请阅读并同意隐私政策 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 4733a634f..cc5b40e81 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -890,6 +890,8 @@ 分享 分享日誌 分享日誌失敗:%1$s + 守護程式日誌不可用 + 找不到守護程式的日誌文件,守護程式是否正在執行? 隱私政策 繼續之前請閱讀並同意隱私政策 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f87ab2185..8a2f24f09 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -927,6 +927,8 @@ Share Share logs Failed to share logs: %1$s + Daemon log unavailable + No daemon log file found, is daemon running? Privacy Policy Please read and agree to the privacy policy before continuing From 71c8e435e4c9fea41042f85e06060cd60f0741d1 Mon Sep 17 00:00:00 2001 From: BigfootACA Date: Sun, 5 Jul 2026 11:32:43 +0800 Subject: [PATCH 098/182] BaseCheckStepFragment: trigger check on resume Signed-off-by: BigfootACA --- .../droidvm/ui/setup/base/BaseCheckStepFragment.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/java/cn/classfun/droidvm/ui/setup/base/BaseCheckStepFragment.java b/app/src/main/java/cn/classfun/droidvm/ui/setup/base/BaseCheckStepFragment.java index eb9c89104..cbd35647c 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/setup/base/BaseCheckStepFragment.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/setup/base/BaseCheckStepFragment.java @@ -33,6 +33,12 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat runCheck(); } + @Override + public void onResume() { + super.onResume(); + runCheck(); + } + protected abstract void runCheck(); @SuppressWarnings("SameReturnValue") From 98c45b19fb3b3d7eea48851022e1b87a89b94219 Mon Sep 17 00:00:00 2001 From: BigfootACA Date: Sun, 5 Jul 2026 11:33:34 +0800 Subject: [PATCH 099/182] PickerButtonWidget: fix button in RTL layout Signed-off-by: BigfootACA --- app/src/main/res/layout/widget_picker_button.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/layout/widget_picker_button.xml b/app/src/main/res/layout/widget_picker_button.xml index 08098dfc7..9525a8844 100644 --- a/app/src/main/res/layout/widget_picker_button.xml +++ b/app/src/main/res/layout/widget_picker_button.xml @@ -11,6 +11,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" + android:layout_alignParentStart="true" android:minWidth="64dp" tools:ignore="SpeakableTextPresentCheck" tools:visibility="visible" /> From fb7628d11ed9e2844836b8baf88cb7e3e2cc18db Mon Sep 17 00:00:00 2001 From: BigfootACA Date: Sun, 5 Jul 2026 11:34:22 +0800 Subject: [PATCH 100/182] TabSwipeHelper: fix swipe in RTL layout Signed-off-by: BigfootACA --- .../droidvm/lib/ui/TabSwipeHelper.java | 66 +++++++++++++------ 1 file changed, 47 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/cn/classfun/droidvm/lib/ui/TabSwipeHelper.java b/app/src/main/java/cn/classfun/droidvm/lib/ui/TabSwipeHelper.java index 9eb6632f2..ff415d81c 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/ui/TabSwipeHelper.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/ui/TabSwipeHelper.java @@ -85,8 +85,10 @@ private boolean onMotionEventMove( float dx = ev.getX() - touchStartX; int pos = callback.getCurrentTabIndex(); int last = callback.getTabCount() - 1; - isDragging = !((dx > 0 && pos == 0) || - (dx < 0 && pos == last)); + int prevSign = prevFingerSign(); + int nextSign = nextFingerSign(); + isDragging = !((pos == 0 && dx * prevSign > 0) || + (pos == last && dx * nextSign > 0)); } if (isDragging) { prepareDrag(); @@ -161,6 +163,19 @@ private int getWidth() { return activity.getWindow().getDecorView().getWidth(); } + private boolean isRtl() { + return activity.getWindow().getDecorView().getLayoutDirection() + == View.LAYOUT_DIRECTION_RTL; + } + + private int prevFingerSign() { + return isRtl() ? -1 : 1; + } + + private int nextFingerSign() { + return isRtl() ? 1 : -1; + } + private void prepareDrag() { int pos = callback.getCurrentTabIndex(); int count = callback.getTabCount(); @@ -169,14 +184,16 @@ private void prepareDrag() { dragNextView = pos < count - 1 ? callback.getTabView(pos + 1) : null; int width = dragCurrentView.getWidth(); if (width <= 0) width = getWidth(); + int prevSign = prevFingerSign(); + int nextSign = nextFingerSign(); if (dragPrevView != null) { dragPrevView.animate().cancel(); - dragPrevView.setTranslationX(-width); + dragPrevView.setTranslationX(-prevSign * width); dragPrevView.setVisibility(View.VISIBLE); } if (dragNextView != null) { dragNextView.animate().cancel(); - dragNextView.setTranslationX(width); + dragNextView.setTranslationX(-nextSign * width); dragNextView.setVisibility(View.VISIBLE); } } @@ -184,25 +201,32 @@ private void prepareDrag() { private void updateDrag(float dx) { if (dragCurrentView == null) return; int width = dragCurrentView.getWidth(); - if (dragPrevView == null && dx > 0) dx = 0; - if (dragNextView == null && dx < 0) dx = 0; + int prevSign = prevFingerSign(); + int nextSign = nextFingerSign(); + if (dragPrevView == null && dx * prevSign > 0) dx = 0; + if (dragNextView == null && dx * nextSign > 0) dx = 0; dragCurrentView.setTranslationX(dx); - if (dragPrevView != null) dragPrevView.setTranslationX(dx - width); - if (dragNextView != null) dragNextView.setTranslationX(dx + width); + if (dragPrevView != null) + dragPrevView.setTranslationX(dx - prevSign * width); + if (dragNextView != null) + dragNextView.setTranslationX(dx - nextSign * width); } private void finishDrag(float dx, float velocityX) { if (dragCurrentView == null) return; int width = dragCurrentView.getWidth(); - if (dragPrevView == null && dx > 0) dx = 0; - if (dragNextView == null && dx < 0) dx = 0; + int prevSign = prevFingerSign(); + int nextSign = nextFingerSign(); + if (dragPrevView == null && dx * prevSign > 0) dx = 0; + if (dragNextView == null && dx * nextSign > 0) dx = 0; boolean goNext = false, goPrev = false; if (Math.abs(velocityX) > SNAP_VELOCITY) { - if (velocityX > 0 && dragPrevView != null) goPrev = true; - else if (velocityX < 0 && dragNextView != null) goNext = true; + if (velocityX * prevSign > 0 && dragPrevView != null) goPrev = true; + else if (velocityX * nextSign > 0 && dragNextView != null) + goNext = true; } else if (Math.abs(dx) > width * SNAP_FRACTION) { - if (dx > 0 && dragPrevView != null) goPrev = true; - else if (dx < 0 && dragNextView != null) goNext = true; + if (dx * prevSign > 0 && dragPrevView != null) goPrev = true; + else if (dx * nextSign > 0 && dragNextView != null) goNext = true; } settling = true; int current = callback.getCurrentTabIndex(); @@ -217,13 +241,14 @@ private void settleDragTo(int targetIdx) { boolean toNext = targetIdx > current; View targetView = toNext ? dragNextView : dragPrevView; View otherView = toNext ? dragPrevView : dragNextView; + int sign = toNext ? nextFingerSign() : prevFingerSign(); if (otherView != null) { otherView.animate().cancel(); otherView.setVisibility(View.GONE); otherView.setTranslationX(0); } dragCurrentView.animate() - .translationX(toNext ? -width : width) + .translationX(sign * width) .setDuration(SETTLE_DURATION) .withEndAction(() -> { dragCurrentView.setVisibility(View.GONE); @@ -242,6 +267,8 @@ private void settleDragTo(int targetIdx) { private void settleBack() { int width = dragCurrentView.getWidth(); + int prevSign = prevFingerSign(); + int nextSign = nextFingerSign(); dragCurrentView.animate() .translationX(0) .setDuration(SETTLE_DURATION) @@ -249,7 +276,7 @@ private void settleBack() { .start(); if (dragPrevView != null) { dragPrevView.animate() - .translationX(-width) + .translationX(-prevSign * width) .setDuration(SETTLE_DURATION) .withEndAction(() -> { dragPrevView.setVisibility(View.GONE); @@ -259,7 +286,7 @@ private void settleBack() { } if (dragNextView != null) { dragNextView.animate() - .translationX(width) + .translationX(-nextSign * width) .setDuration(SETTLE_DURATION) .withEndAction(() -> { dragNextView.setVisibility(View.GONE); @@ -334,6 +361,7 @@ private void doAnimateStep( View inView = callback.getTabView(inIdx); int width = outView.getWidth(); if (width <= 0) width = getWidth(); + int sign = direction > 0 ? nextFingerSign() : prevFingerSign(); Runnable animateEndAction = () -> { outView.setVisibility(View.GONE); outView.setTranslationX(0); @@ -347,11 +375,11 @@ private void doAnimateStep( } }; outView.animate() - .translationX(-direction * width) + .translationX(sign * width) .setDuration(duration) .withEndAction(animateEndAction) .start(); - inView.setTranslationX(direction * width); + inView.setTranslationX(-sign * width); inView.setVisibility(View.VISIBLE); inView.animate() .translationX(0) From e3969924ce4cf11ab810ed2c1ab87d09e34014d9 Mon Sep 17 00:00:00 2001 From: BigfootACA Date: Sun, 5 Jul 2026 11:37:44 +0800 Subject: [PATCH 101/182] StringUtils: resolveUriPath: try open target file to read path Signed-off-by: BigfootACA --- .../droidvm/lib/utils/StringUtils.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/app/src/main/java/cn/classfun/droidvm/lib/utils/StringUtils.java b/app/src/main/java/cn/classfun/droidvm/lib/utils/StringUtils.java index 3d1a4613e..3d49a8c9f 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/utils/StringUtils.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/utils/StringUtils.java @@ -3,6 +3,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Base64.getDecoder; import static java.util.Base64.getEncoder; +import static java.util.Objects.requireNonNull; import static cn.classfun.droidvm.lib.utils.FileUtils.copyStream; import static cn.classfun.droidvm.lib.utils.FileUtils.externalPath; import static cn.classfun.droidvm.lib.utils.FileUtils.shellCheckExists; @@ -10,6 +11,7 @@ import android.content.Context; import android.net.Uri; import android.provider.DocumentsContract; +import android.system.Os; import android.widget.EditText; import androidx.annotation.NonNull; @@ -82,6 +84,26 @@ private static String parseTreePath(@Nullable String content, @Nullable String t return null; } + @Nullable + private static String tryResolvePathFromFd( + @NonNull Context ctx, + @NonNull Uri uri, + @NonNull String mode + ) throws Exception { + var resolver = ctx.getContentResolver(); + try (var pfd = resolver.openFileDescriptor(uri, mode)) { + requireNonNull(pfd); + var fdp = fmt("/proc/self/fd/%d", pfd.getFd()); + var path = Os.readlink(fdp); + if ( + shellCheckExists(path) && + path.startsWith("/") && + !path.endsWith(" (deleted)") + ) return path; + } + return null; + } + public static String resolveUriPath(Context ctx, Uri uri) { String part; try { @@ -103,6 +125,16 @@ public static String resolveUriPath(Context ctx, Uri uri) { if ((part = parseTreePath(path, "/document/primary:")) != null) return part; if ((part = parseTreePath(path, "/external_files/")) != null) return part; if ((part = parseTreePath(path, "/tree/primary:")) != null) return part; + try { + var real = tryResolvePathFromFd(ctx, uri, "r"); + if (real != null) return real; + } catch (Exception ignored) { + } + try { + var real = tryResolvePathFromFd(ctx, uri, "rw"); + if (real != null) return real; + } catch (Exception ignored) { + } return path; } From d3ee31b26d9273212ff1be248e83f2e0480552d0 Mon Sep 17 00:00:00 2001 From: BigfootACA Date: Sun, 5 Jul 2026 11:39:42 +0800 Subject: [PATCH 102/182] JsonUtils: add listToJSONArray and mapToJSONObject Signed-off-by: BigfootACA --- .../classfun/droidvm/lib/utils/JsonUtils.java | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/app/src/main/java/cn/classfun/droidvm/lib/utils/JsonUtils.java b/app/src/main/java/cn/classfun/droidvm/lib/utils/JsonUtils.java index 9d5ef9ccb..86d11638e 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/utils/JsonUtils.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/utils/JsonUtils.java @@ -13,6 +13,8 @@ import java.util.Locale; import java.util.Map; +import cn.classfun.droidvm.lib.store.base.JSONSerialize; + public final class JsonUtils { @NonNull @SuppressWarnings("unused") @@ -277,6 +279,33 @@ public static List arrayToList( return arrayToList(obj.getJSONArray(key), cb); } + @NonNull + @SuppressWarnings("unused") + public static JSONArray listToJSONArray( + @NonNull List list + ) throws JSONException { + var arr = new JSONArray(); + for (var item : list) + arr.put(item.toJson()); + return arr; + } + + @NonNull + @SuppressWarnings("unused") + public static JSONObject mapToJSONObject( + @NonNull Map map + ) throws JSONException { + var obj = new JSONObject(); + for (var key : map.keySet()) { + var value = JSONObject.NULL; + var item = map.get(key); + if (item != null) + value = item.toJson(); + obj.put(key, value); + } + return obj; + } + public interface ObjectValueMapper { @Nullable @SuppressWarnings({"RedundantThrows", "unused"}) From 37cc82ff740bde37940175e600106d1b97df24a9 Mon Sep 17 00:00:00 2001 From: BigfootACA Date: Mon, 6 Jul 2026 23:59:40 +0800 Subject: [PATCH 103/182] LogHelper: fix event handle without vm_id Signed-off-by: BigfootACA --- .../cn/classfun/droidvm/lib/diag/LogHelper.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/cn/classfun/droidvm/lib/diag/LogHelper.java b/app/src/main/java/cn/classfun/droidvm/lib/diag/LogHelper.java index 1f347e817..35c61338d 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/diag/LogHelper.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/diag/LogHelper.java @@ -51,21 +51,23 @@ private LogContext(UUID vmId) { @Override public void onDaemonEvent(@NonNull JSONObject msg) { - var type = msg.optString("type", ""); + var type = msg.optString("type"); if (!type.equals("event")) return; var data = msg.optJSONObject("data"); if (data == null) return; - var vmId = UUID.fromString(data.optString("vm_id", "")); - var vmName = data.optString("vm_name", ""); - var event = data.optString("event", ""); + var vmIdStr = data.optString("vm_id"); + if (vmIdStr.isEmpty()) return; + var vmId = UUID.fromString(vmIdStr); + var vmName = data.optString("vm_name"); + var event = data.optString("event"); if (event.equals("exited")) { vmLogContexts.remove(vmId); return; } var logs = vmLogContexts.computeIfAbsent(vmId, k -> new LogContext(vmId)); if (!event.equals("output")) return; - var text = URLDecoder.decode(data.optString("data", ""), StandardCharsets.UTF_8); - var stream = data.optString("stream", ""); + var text = URLDecoder.decode(data.optString("data"), StandardCharsets.UTF_8); + var stream = data.optString("stream"); if (stream.isEmpty()) return; var buff = logs.log.computeIfAbsent(stream, k -> new RingBuffer(4096)); buff.adds(text.getBytes(StandardCharsets.UTF_8)); From 2d919f6cc8b12cae4db96a53b587742fcd456b46 Mon Sep 17 00:00:00 2001 From: BigfootACA Date: Tue, 7 Jul 2026 00:00:15 +0800 Subject: [PATCH 104/182] DiskStore: allow non unique disk name Signed-off-by: BigfootACA --- .../java/cn/classfun/droidvm/lib/store/base/DataStore.java | 5 +++++ .../java/cn/classfun/droidvm/lib/store/disk/DiskStore.java | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/base/DataStore.java b/app/src/main/java/cn/classfun/droidvm/lib/store/base/DataStore.java index a752b13cc..87a1949a1 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/base/DataStore.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/base/DataStore.java @@ -270,6 +270,7 @@ public final boolean isNameUnique(String name) { } public final boolean isNameUnique(String name, List excludes) { + if (!shouldNameUnique()) return true; if (name == null) return false; for (var item : dataMap) { if (excludes != null && excludes.contains(item.getId())) @@ -309,6 +310,10 @@ public final boolean equals(Object obj) { return dataMap.equals(other.dataMap); } + protected boolean shouldNameUnique() { + return true; + } + @NonNull @SuppressWarnings("unused") protected abstract T create(); diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/disk/DiskStore.java b/app/src/main/java/cn/classfun/droidvm/lib/store/disk/DiskStore.java index 955798df1..1dce22b4b 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/disk/DiskStore.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/disk/DiskStore.java @@ -32,6 +32,11 @@ public DiskStore(@NonNull Context context) { super(context); } + @Override + protected boolean shouldNameUnique() { + return false; + } + @NonNull @Override protected DiskConfig create() { From 998a133424e6a59c23d2a84115ea4570db020aa3 Mon Sep 17 00:00:00 2001 From: BigfootACA Date: Tue, 7 Jul 2026 00:00:53 +0800 Subject: [PATCH 105/182] ShareUtils: refactor and add shareFile Signed-off-by: BigfootACA --- .../droidvm/lib/utils/ShareUtils.java | 62 +++++++++++++------ app/src/main/res/xml/file_paths.xml | 7 ++- 2 files changed, 48 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/cn/classfun/droidvm/lib/utils/ShareUtils.java b/app/src/main/java/cn/classfun/droidvm/lib/utils/ShareUtils.java index 66d28397b..ee824e39a 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/utils/ShareUtils.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/utils/ShareUtils.java @@ -9,34 +9,30 @@ import android.os.Handler; import android.os.Looper; import android.util.Log; +import android.webkit.MimeTypeMap; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.core.content.FileProvider; import java.io.File; import java.io.FileOutputStream; import java.nio.charset.StandardCharsets; -/** Helpers for exporting in-app text to the system share sheet (ACTION_SEND). */ public final class ShareUtils { private static final String TAG = "ShareUtils"; - private static final String SHARE_DIR = "shared_logs"; + private static final String SHARE_DIR = "shared"; private static final Handler MAIN = new Handler(Looper.getMainLooper()); private ShareUtils() { } - /** - * Write {@code content} to a temp file under the cache dir and launch the system - * share sheet with it as a {@code text/plain} attachment. File write runs off the - * main thread; the chooser is launched back on the main thread. {@code onError} is - * invoked on the main thread with a message when sharing fails. - */ public static void shareTextAsFile( - Context context, - String filename, - String content, - String chooserTitle, - OnError onError + @NonNull Context context, + @NonNull String filename, + @NonNull String content, + @NonNull String chooserTitle, + @Nullable OnError onError ) { var appContext = context.getApplicationContext(); runOnPool(() -> { @@ -50,9 +46,7 @@ public static void shareTextAsFile( os.write(content.getBytes(StandardCharsets.UTF_8)); os.flush(); } - var authority = fmt("%s.fileprovider", appContext.getPackageName()); - var uri = FileProvider.getUriForFile(appContext, authority, file); - MAIN.post(() -> launchChooser(context, uri, chooserTitle, onError)); + shareFile(context, file, null, chooserTitle, onError); } catch (Exception e) { Log.e(TAG, "Failed to prepare shared file", e); MAIN.post(() -> { @@ -62,10 +56,41 @@ public static void shareTextAsFile( }); } - private static void launchChooser(Context context, Uri uri, String title, OnError onError) { + public static void shareFile( + @NonNull Context context, + @NonNull File file, + @Nullable String mime, + @NonNull String chooserTitle, + @Nullable OnError onError + ) { + try { + var appContext = context.getApplicationContext(); + var authority = fmt("%s.fileprovider", appContext.getPackageName()); + var uri = FileProvider.getUriForFile(appContext, authority, file); + MAIN.post(() -> launchChooser(context, uri, chooserTitle, mime, onError)); + } catch (Exception e) { + Log.e(TAG, "Failed to create share uri", e); + MAIN.post(() -> { + if (onError != null) onError.onError(e.getMessage()); + }); + } + } + + public static void launchChooser( + @NonNull Context context, + @NonNull Uri uri, + @NonNull String title, + @Nullable String mime, + @Nullable OnError onError + ) { + if (mime == null) { + var ext = MimeTypeMap.getFileExtensionFromUrl(uri.toString()); + if (ext != null) mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext); + if (mime == null) mime = "application/octet-stream"; + } try { var send = new Intent(Intent.ACTION_SEND) - .setType("text/plain") + .setType(mime) .putExtra(Intent.EXTRA_STREAM, uri) .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); var chooser = Intent.createChooser(send, title); @@ -77,7 +102,6 @@ private static void launchChooser(Context context, Uri uri, String title, OnErro } } - /** Drop previously exported files so the cache dir does not grow unbounded. */ private static void pruneOldFiles(File dir) { var files = dir.listFiles(); if (files == null) return; diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml index fa1e5caaa..f55980838 100644 --- a/app/src/main/res/xml/file_paths.xml +++ b/app/src/main/res/xml/file_paths.xml @@ -2,6 +2,9 @@ + name="shared" + path="shared/" /> + From 2fa35c991f555e166a109609bfb4987526afc49a Mon Sep 17 00:00:00 2001 From: BigfootACA Date: Tue, 7 Jul 2026 00:01:53 +0800 Subject: [PATCH 106/182] utils: add BinaryUtils Signed-off-by: BigfootACA --- .../droidvm/lib/utils/BinaryUtils.java | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 app/src/main/java/cn/classfun/droidvm/lib/utils/BinaryUtils.java diff --git a/app/src/main/java/cn/classfun/droidvm/lib/utils/BinaryUtils.java b/app/src/main/java/cn/classfun/droidvm/lib/utils/BinaryUtils.java new file mode 100644 index 000000000..38a7c7d7f --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/lib/utils/BinaryUtils.java @@ -0,0 +1,109 @@ +package cn.classfun.droidvm.lib.utils; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; + +import androidx.annotation.NonNull; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.nio.charset.CodingErrorAction; + +public class BinaryUtils { + private static final byte[] ZERO = new byte[64 * 1024]; + private BinaryUtils() { + + } + + public static long alignUp(long value) { + return alignUp(value, 0x1000); + } + + public static long alignUp(long value, long align) { + long mask = align - 1L; + return (value + mask) & ~mask; + } + + public static long alignUpStrict(long value) { + return alignUpStrict(value, 0x1000); + } + + public static long alignUpStrict(long value, long align) { + long aligned = alignUp(value); + return aligned == value ? value + align : aligned; + } + + public static void writeZero(@NonNull RandomAccessFile raf, long count) throws IOException { + long rem = count; + while (rem > 0) { + int n = (int) Math.min(ZERO.length, rem); + raf.write(ZERO, 0, n); + rem -= n; + } + } + + @NonNull + public static String decodeUtf8(@NonNull byte[] bytes) throws IOException { + var decoder = UTF_8.newDecoder() + .onMalformedInput(CodingErrorAction.REPORT) + .onUnmappableCharacter(CodingErrorAction.REPORT); + try { + return decoder.decode(ByteBuffer.wrap(bytes)).toString(); + } catch (Exception e) { + throw new IOException("vmpkg manifest is not valid UTF-8", e); + } + } + + public static void readZeroPadding(@NonNull InputStream in, long n) throws IOException { + var buf = new byte[64 * 1024]; + long rem = n; + while (rem > 0) { + int toRead = (int) Math.min(buf.length, rem); + int read = in.read(buf, 0, toRead); + if (read < 0) throw new EOFException("unexpected end of vmpkg padding"); + for (int i = 0; i < read; i++) + if (buf[i] != 0) throw new IOException("vmpkg padding is not zero-filled"); + rem -= read; + } + } + + public static int readSome(@NonNull InputStream in, @NonNull byte[] buf) throws IOException { + int n = 0; + while (n < buf.length) { + int r = in.read(buf, n, buf.length - n); + if (r < 0) break; + n += r; + } + return n; + } + + public static void readFully(@NonNull InputStream in, @NonNull byte[] buf) throws IOException { + int n = readSome(in, buf); + if (n != buf.length) throw new EOFException("unexpected end of vmpkg"); + } + + public static int getUInt16LE(@NonNull byte[] buf, int off) { + return (buf[off] & 0xff) | ((buf[off + 1] & 0xff) << 8); + } + + public static long getInt64LE(@NonNull byte[] buf, int off) { + long v = 0; + for (int i = 7; i >= 0; i--) v = (v << 8) | (buf[off + i] & 0xffL); + return v; + } + + public static void putUInt16LE(@NonNull byte[] buf, int off, int value) throws IOException { + if (value < 0 || value > 0xffff) + throw new IOException(fmt("uint16 out of range: %d", value)); + buf[off] = (byte) (value & 0xff); + buf[off + 1] = (byte) ((value >>> 8) & 0xff); + } + + public static void putInt64LE(@NonNull byte[] buf, int off, long value) throws IOException { + if (value < 0) throw new IOException(fmt("uint64 out of range: %d", value)); + for (int i = 0; i < 8; i++) buf[off + i] = (byte) ((value >>> (i * 8)) & 0xff); + } +} From f43bbcf1b07fb91f050cbb1d08f8653585051753 Mon Sep 17 00:00:00 2001 From: BigfootACA Date: Tue, 7 Jul 2026 11:13:05 +0800 Subject: [PATCH 107/182] java: fix all deprecated api usage Signed-off-by: BigfootACA --- .../droidvm/daemon/display/DaemonSystemContext.java | 4 ++-- .../droidvm/ui/main/settings/MainSettingsFragment.java | 6 ++++-- .../classfun/droidvm/ui/vm/console/VMConsoleActivity.java | 3 +-- .../nativedisplay/display/VMNativeDisplayActivity.java | 4 +--- .../droidvm/ui/vm/display/vnc/base/BaseVncActivity.java | 3 +-- .../cn/classfun/droidvm/ui/vm/edit/boot/VMEditBootTab.java | 2 +- 6 files changed, 10 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/display/DaemonSystemContext.java b/app/src/main/java/cn/classfun/droidvm/daemon/display/DaemonSystemContext.java index 3437a54aa..e5e4c1f07 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/display/DaemonSystemContext.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/display/DaemonSystemContext.java @@ -33,9 +33,9 @@ private DaemonSystemContext() { public static synchronized void init() { if (context != null) return; try { - if (Looper.myLooper() == null) Looper.prepareMainLooper(); + if (Looper.myLooper() == null) Looper.prepare(); } catch (Throwable t) { - Log.w(TAG, fmt("prepareMainLooper: %s", t)); + Log.w(TAG, fmt("prepare looper: %s", t)); } try { Class at = Class.forName("android.app.ActivityThread"); diff --git a/app/src/main/java/cn/classfun/droidvm/ui/main/settings/MainSettingsFragment.java b/app/src/main/java/cn/classfun/droidvm/ui/main/settings/MainSettingsFragment.java index 4639280e5..617628e24 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/main/settings/MainSettingsFragment.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/main/settings/MainSettingsFragment.java @@ -509,8 +509,10 @@ private void loadStore( @NonNull JSONObject json ) throws Exception { var ctx = requireContext().getApplicationContext(); - var oldStore = store.newInstance(); - var newStore = store.newInstance(); + var ctor = store.getDeclaredConstructor(); + ctor.setAccessible(true); + var oldStore = ctor.newInstance(); + var newStore = ctor.newInstance(); oldStore.load(ctx); newStore.load(json); newStore.forEach((k, v) -> { diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/console/VMConsoleActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/console/VMConsoleActivity.java index b3c8099bf..12fcaba1a 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/console/VMConsoleActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/console/VMConsoleActivity.java @@ -2,7 +2,6 @@ import static android.view.HapticFeedbackConstants.KEYBOARD_TAP; import static android.view.KeyEvent.*; -import static android.view.inputmethod.InputMethodManager.SHOW_IMPLICIT; import static android.widget.Toast.LENGTH_SHORT; import static java.util.Objects.requireNonNull; import static cn.classfun.droidvm.lib.ui.MaterialMenu.setupToolbarMenu; @@ -107,7 +106,7 @@ public void onSingleTapUp(MotionEvent e) { var imm = getSystemService(InputMethodManager.class); if (imm != null && terminalView != null) { terminalView.requestFocus(); - imm.showSoftInput(terminalView, SHOW_IMPLICIT); + imm.showSoftInput(terminalView, 0); } } diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/display/VMNativeDisplayActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/display/VMNativeDisplayActivity.java index 25b19cee7..12f0622f2 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/display/VMNativeDisplayActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/display/VMNativeDisplayActivity.java @@ -427,11 +427,9 @@ private void toggleSoftKeyboard() { // resort. private void tryShowKeyboard(@NonNull InputMethodManager imm, int attemptsLeft) { keyboardInput.requestFocus(); - if (imm.showSoftInput(keyboardInput, InputMethodManager.SHOW_IMPLICIT)) return; + if (imm.showSoftInput(keyboardInput, 0)) return; if (attemptsLeft > 0) { mainHandler.postDelayed(() -> tryShowKeyboard(imm, attemptsLeft - 1), 60); - } else { - imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0); } } diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/base/BaseVncActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/base/BaseVncActivity.java index 9fafe5eb7..e5447a5d7 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/base/BaseVncActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/base/BaseVncActivity.java @@ -5,7 +5,6 @@ import static android.view.KeyEvent.*; import static android.view.View.GONE; import static android.view.View.VISIBLE; -import static android.view.inputmethod.InputMethodManager.SHOW_IMPLICIT; import static org.yaml.snakeyaml.util.UriEncoder.encode; import static java.util.concurrent.Executors.newSingleThreadExecutor; import static java.util.concurrent.TimeUnit.SECONDS; @@ -540,7 +539,7 @@ protected void toggleSoftKeyboard() { var imm = getSystemService(InputMethodManager.class); if (imm != null && ivDisplay != null) { ivDisplay.requestFocus(); - imm.showSoftInput(ivDisplay, SHOW_IMPLICIT); + imm.showSoftInput(ivDisplay, 0); } } diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/boot/VMEditBootTab.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/boot/VMEditBootTab.java index e1939c240..ace779d7b 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/boot/VMEditBootTab.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/boot/VMEditBootTab.java @@ -454,7 +454,7 @@ private void redirectCmdlineEditToOverride(int caret) { etCmdlineOverride.setSelection(Math.max(0, Math.min(caret, etCmdlineOverride.length()))); var imm = (InputMethodManager) parent.getSystemService(Context.INPUT_METHOD_SERVICE); if (imm != null) - imm.showSoftInput(etCmdlineOverride, InputMethodManager.SHOW_IMPLICIT); + imm.showSoftInput(etCmdlineOverride, 0); } private void updateDetectionCard() { From aa1e0d656947d98bf9dfdb40ea7a7c74f3eddd85 Mon Sep 17 00:00:00 2001 From: BigfootACA Date: Tue, 7 Jul 2026 11:55:12 +0800 Subject: [PATCH 108/182] AssetUtils: refactor libraries extract Signed-off-by: BigfootACA --- app/build.gradle.kts | 17 ------------ .../cn/classfun/droidvm/lib/Constants.java | 9 ------- .../droidvm/lib/daemon/DaemonHelper.java | 8 +++++- .../droidvm/lib/utils/AssetUtils.java | 27 ++++++++++++++++++- 4 files changed, 33 insertions(+), 28 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 76309e640..bc2fe88a5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -138,23 +138,6 @@ abstract class CopyNativeBinAssetsTask : DefaultTask() { dest.parentFile.mkdirs() src.copyTo(dest, overwrite = true) } - val libraries = setOf( - "libsimpledump.so", - "libunixhelper.so", - "libcompat_a13.so", - "libcompat_a14.so", - "libcompat_a15.so", - "libcompat_a16.so", - "libcompat_a17.so", - ) - cmakeDir.walkTopDown() - .filter { it.name in libraries && it.isFile } - .forEach { src -> - val abi = src.parentFile.name - val dest = File(outDir, "lib/$abi/${src.name}") - dest.parentFile.mkdirs() - src.copyTo(dest, overwrite = true) - } } } diff --git a/app/src/main/java/cn/classfun/droidvm/lib/Constants.java b/app/src/main/java/cn/classfun/droidvm/lib/Constants.java index 9256ea2c1..5ce2353db 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/Constants.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/Constants.java @@ -23,15 +23,6 @@ public final class Constants { // third-party daemons (gvswitch/pbridge/bridgedhcp/netbox) and lbx now // arrive via the prebuilt tar.xz, extracted to DATA/bin/. public static final String[] BINARIES_BUILT = {"droidvm", "daemon"}; - public static final String[] LIBRARIES_BUILT = { - "libsimpledump.so", - "libunixhelper.so", - "libcompat_a13.so", - "libcompat_a14.so", - "libcompat_a15.so", - "libcompat_a16.so", - "libcompat_a17.so", - }; /** * IFA_RT_PRIORITY magic that pbridge tags its ND/ARP-offload proxy * addresses with (its --offload-workaround-magic). Host-IP discovery diff --git a/app/src/main/java/cn/classfun/droidvm/lib/daemon/DaemonHelper.java b/app/src/main/java/cn/classfun/droidvm/lib/daemon/DaemonHelper.java index 0ff2b882c..3668efab7 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/daemon/DaemonHelper.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/daemon/DaemonHelper.java @@ -132,9 +132,15 @@ public boolean startDaemon(boolean force) { run("mkdir -p %s", runDir); // Best-effort ownership fix; chown may be denied on some contexts (e.g. Android). run("chown %d:%d %s || true", uid, uid, runDir); + var libPath = fmt( + "%s:%s", + ctx.getContext().getApplicationInfo().nativeLibraryDir, + pathJoin(DATA_DIR, "lib") + ); var cmd = fmt( - "env CLASSPATH=%s %s %s / %s%s", + "env CLASSPATH=%s LD_LIBRARY_PATH=%s %s %s / %s%s", escapedString(zipPath), + escapedString(libPath), escapedString(getAssetBinaryPath("daemon")), escapedString(findExecute("app_process64")), escapedString(Daemon.class.getName()), diff --git a/app/src/main/java/cn/classfun/droidvm/lib/utils/AssetUtils.java b/app/src/main/java/cn/classfun/droidvm/lib/utils/AssetUtils.java index 4b04513c0..9a6afaebf 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/utils/AssetUtils.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/utils/AssetUtils.java @@ -20,6 +20,7 @@ import java.io.BufferedInputStream; import java.io.File; +import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; @@ -204,8 +205,32 @@ public static void extractBinaries(@NonNull Context context) { extractAsset(context, "bin", BINARIES_BUILT, false); } + @SuppressLint("SetWorldReadable") public static void extractLibraries(@NonNull Context context) { - extractAsset(context, "lib", LIBRARIES_BUILT, false); + var srcDir = new File(context.getApplicationInfo().nativeLibraryDir); + var srcFiles = srcDir.listFiles((dir, name) -> name.endsWith(".so")); + if (srcFiles == null || srcFiles.length == 0) { + Log.w(TAG, fmt("No installed native library in %s", srcDir)); + return; + } + var dstDir = new File(context.getDataDir(), "lib"); + if (!dstDir.exists() && !dstDir.mkdirs()) + Log.w(TAG, fmt("Failed to create native library dir: %s", dstDir)); + for (var src : srcFiles) { + var dst = new File(dstDir, src.getName()); + try { + if (dst.exists() && !dst.delete()) + Log.w(TAG, fmt("Could not unlink old native library: %s", dst)); + try (var in = new FileInputStream(src); var out = new FileOutputStream(dst)) { + copyStream(in, out); + } + if (!dst.setReadable(true, false)) + Log.w(TAG, "setReadable failed"); + Log.i(TAG, fmt("Extracted installed native library %s to %s", src, dst)); + } catch (Exception e) { + Log.e(TAG, fmt("Failed to extract installed native library: %s", src), e); + } + } } public static boolean needsExtractPrebuilt(@NonNull Context context) { From 4aa3efbbb2b17a38b51c76771f63a9106fe4b1f2 Mon Sep 17 00:00:00 2001 From: BigfootACA Date: Tue, 7 Jul 2026 15:24:31 +0800 Subject: [PATCH 109/182] lib: add archive for tarball Signed-off-by: BigfootACA --- app/build.gradle.kts | 2 + .../droidvm/lib/archive/Compression.java | 90 +++++++++ .../lib/archive/LimitedInputStream.java | 63 ++++++ .../lib/archive/OutputStreamLimiter.java | 49 +++++ .../archive/RandomAccessFileOutputStream.java | 26 +++ .../droidvm/lib/archive/TarReader.java | 145 ++++++++++++++ .../droidvm/lib/archive/TarWriter.java | 183 ++++++++++++++++++ gradle/libs.versions.toml | 2 + 8 files changed, 560 insertions(+) create mode 100644 app/src/main/java/cn/classfun/droidvm/lib/archive/Compression.java create mode 100644 app/src/main/java/cn/classfun/droidvm/lib/archive/LimitedInputStream.java create mode 100644 app/src/main/java/cn/classfun/droidvm/lib/archive/OutputStreamLimiter.java create mode 100644 app/src/main/java/cn/classfun/droidvm/lib/archive/RandomAccessFileOutputStream.java create mode 100644 app/src/main/java/cn/classfun/droidvm/lib/archive/TarReader.java create mode 100644 app/src/main/java/cn/classfun/droidvm/lib/archive/TarWriter.java diff --git a/app/build.gradle.kts b/app/build.gradle.kts index bc2fe88a5..aac83d6e7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -337,6 +337,8 @@ dependencies { implementation(libs.okhttp3) implementation(libs.snakeyaml) implementation(libs.xz) + implementation(libs.zstd) { artifact { type = "aar" } } + testImplementation(libs.zstd) implementation(libs.termux.emulator) implementation(libs.termux.view) testImplementation(libs.junit) diff --git a/app/src/main/java/cn/classfun/droidvm/lib/archive/Compression.java b/app/src/main/java/cn/classfun/droidvm/lib/archive/Compression.java new file mode 100644 index 000000000..059234a2c --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/lib/archive/Compression.java @@ -0,0 +1,90 @@ +package cn.classfun.droidvm.lib.archive; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; + +import cn.classfun.droidvm.R; +import cn.classfun.droidvm.lib.store.enums.StringEnum; + +public enum Compression implements StringEnum { + NONE( + /* ext = */ "bin", + /* mime = */ "application/octet-stream", + /* id = */ "none", + /* type = */ 0, + /* display = */ R.string.vmpkg_export_compression_none, + /* desc = */ R.string.nullptr + ), + GZIP( + /* ext = */ "gz", + /* mime = */ "application/gzip", + /* id = */ "gzip", + /* type = */ 1, + /* display = */ R.string.vmpkg_export_compression_gzip, + /* desc = */ R.string.vmpkg_export_compression_gzip_desc + ), + XZ( + /* ext = */ "xz", + /* mime = */ "application/x-xz", + /* id = */ "xz", + /* type = */ 2, + /* display = */ R.string.vmpkg_export_compression_xz, + /* desc = */ R.string.vmpkg_export_compression_xz_desc + ), + ZSTD( + /* ext = */ "zst", + /* mime = */ "application/zstd", + /* id = */ "zstd", + /* type = */ 3, + /* display = */ R.string.vmpkg_export_compression_zstd, + /* desc = */ R.string.vmpkg_export_compression_zstd_desc + ); + + @NonNull + public final String extSuffix; + @NonNull + public final String mime; + @NonNull + public final String id; + public final int type; + @StringRes + public final int display; + @StringRes + public final int desc; + + Compression( + @NonNull String extSuffix, + @NonNull String mime, + @NonNull String id, + int type, + @StringRes int display, + @StringRes int desc + ) { + this.extSuffix = extSuffix; + this.mime = mime; + this.id = id; + this.type = type; + this.display = display; + this.desc = desc; + } + + @Override + public String getString() { + return id; + } + + @NonNull + @Override + public String getDisplayString(@NonNull Context ctx) { + return ctx.getString(display); + } + + @Nullable + public static Compression fromType(int type) { + for (var c : values()) if (c.type == type) return c; + return null; + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/lib/archive/LimitedInputStream.java b/app/src/main/java/cn/classfun/droidvm/lib/archive/LimitedInputStream.java new file mode 100644 index 000000000..451ccfa0c --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/lib/archive/LimitedInputStream.java @@ -0,0 +1,63 @@ +package cn.classfun.droidvm.lib.archive; + +import androidx.annotation.NonNull; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; + +public final class LimitedInputStream extends InputStream { + private final InputStream in; + private long remaining; + private boolean closed = false; + + public LimitedInputStream(@NonNull InputStream in, long size) { + this.in = in; + this.remaining = size; + } + + @Override + public int read() throws IOException { + if (closed) return -1; + if (remaining <= 0) return -1; + int b = in.read(); + if (b >= 0) remaining--; + return b; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + if (closed || remaining <= 0) return -1; + int toRead = (int) Math.min(len, remaining); + int n = in.read(b, off, toRead); + if (n > 0) remaining -= n; + return n; + } + + @Override + public int read(byte[] b) throws IOException { + return read(b, 0, b.length); + } + + @Override + public void close() { + closed = true; + } + + public long getRemaining() { + return remaining; + } + + public void drain() throws IOException { + while (remaining > 0) { + long r = in.skip(remaining); + if (r <= 0) { + if (in.read() < 0) + throw new EOFException("unexpected end of tar entry"); + remaining--; + } else { + remaining -= r; + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/cn/classfun/droidvm/lib/archive/OutputStreamLimiter.java b/app/src/main/java/cn/classfun/droidvm/lib/archive/OutputStreamLimiter.java new file mode 100644 index 000000000..8abc2991d --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/lib/archive/OutputStreamLimiter.java @@ -0,0 +1,49 @@ +package cn.classfun.droidvm.lib.archive; + +import androidx.annotation.NonNull; + +import java.io.IOException; +import java.io.OutputStream; + +public final class OutputStreamLimiter extends OutputStream { + private final OutputStream out; + private long remaining; + private boolean closed = false; + + public OutputStreamLimiter(@NonNull OutputStream out, long size) { + this.out = out; + this.remaining = size; + } + + @Override + public void write(int b) throws IOException { + if (closed || remaining <= 0) + throw new IOException("entry overwrote its declared size"); + out.write(b); + remaining--; + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + if (closed) + throw new IOException("entry closed"); + if (len > remaining) + throw new IOException("entry overwrote its declared size"); + out.write(b, off, len); + remaining -= len; + } + + @Override + public void write(byte[] b) throws IOException { + write(b, 0, b.length); + } + + @Override + public void close() { + closed = true; + } + + public long getRemaining() { + return remaining; + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/lib/archive/RandomAccessFileOutputStream.java b/app/src/main/java/cn/classfun/droidvm/lib/archive/RandomAccessFileOutputStream.java new file mode 100644 index 000000000..6d32422cf --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/lib/archive/RandomAccessFileOutputStream.java @@ -0,0 +1,26 @@ +package cn.classfun.droidvm.lib.archive; + +import androidx.annotation.NonNull; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.RandomAccessFile; + +public final class RandomAccessFileOutputStream extends OutputStream { + @NonNull + private final RandomAccessFile file; + + public RandomAccessFileOutputStream(@NonNull RandomAccessFile file) { + this.file = file; + } + + @Override + public void write(int b) throws IOException { + file.write(b); + } + + @Override + public void write(@NonNull byte[] b, int off, int len) throws IOException { + file.write(b, off, len); + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/lib/archive/TarReader.java b/app/src/main/java/cn/classfun/droidvm/lib/archive/TarReader.java new file mode 100644 index 000000000..34f9b55e5 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/lib/archive/TarReader.java @@ -0,0 +1,145 @@ +package cn.classfun.droidvm.lib.archive; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import androidx.annotation.NonNull; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; + +public final class TarReader implements AutoCloseable { + private static final int BLOCK = 512; + private final InputStream in; + private boolean closed = false; + private boolean eof = false; + + public TarReader(@NonNull InputStream in) { + this.in = in; + } + + public interface EntryHandler { + void handle( + @NonNull String name, + int mode, + long size, + @NonNull InputStream content + ) throws Exception; + } + + public void forEach(@NonNull EntryHandler handler) throws Exception { + while (!eof) { + var hdr = readBlock(); + if (isNullBlock(hdr)) { + eof = true; + return; + } + String name = cstr(hdr, 0, 100); + int type = hdr[156] & 0xff; + long size = parseOctal(hdr, 124, 12); + int mode = (int) parseOctal(hdr, 100, 8); + if (type == 'L') { + var nameBuf = new byte[(int) size]; + readFully(nameBuf); + skipPad(size); + name = new String(nameBuf, UTF_8).trim(); + hdr = readBlock(); + if (isNullBlock(hdr)) { + eof = true; + return; + } + size = parseOctal(hdr, 124, 12); + mode = (int) parseOctal(hdr, 100, 8); + type = hdr[156] & 0xff; + } + if (type != '0' && type != 0) { + skipFully(size); + skipPad(size); + continue; + } + var limited = new LimitedInputStream(in, size); + handler.handle(name, mode, size, limited); + limited.drain(); + skipPad(size); + } + } + + private byte[] readBlock() throws IOException { + var buf = new byte[BLOCK]; + int n = 0; + while (n < BLOCK) { + int r = in.read(buf, n, BLOCK - n); + if (r < 0) { + if (n == 0) { + eof = true; + return buf; + } + throw new EOFException("unexpected end of tar header"); + } + n += r; + } + return buf; + } + + private void readFully(@NonNull byte[] buf) throws IOException { + int n = 0; + while (n < buf.length) { + int r = in.read(buf, n, buf.length - n); + if (r < 0) throw new EOFException("unexpected end of tar entry"); + n += r; + } + } + + private void skipFully(long n) throws IOException { + long rem = n; + while (rem > 0) { + long r = in.skip(rem); + if (r <= 0) { + if (in.read() < 0) + throw new EOFException("unexpected end of tar entry"); + rem--; + } else { + rem -= r; + } + } + } + + private void skipPad(long size) throws IOException { + int pad = (int) ((BLOCK - (size % BLOCK)) % BLOCK); + if (pad > 0) skipFully(pad); + } + + private static boolean isNullBlock(@NonNull byte[] hdr) { + for (int i = 0; i < BLOCK; i++) + if (hdr[i] != 0) + return false; + return true; + } + + @NonNull + private static String cstr(@NonNull byte[] hdr, int off, int len) { + var end = off; + while (end < off + len && hdr[end] != 0) end++; + if (end == off) return ""; + return new String(hdr, off, end - off, UTF_8); + } + + private static long parseOctal(@NonNull byte[] hdr, int off, int len) { + var s = cstr(hdr, off, len); + if (s.isEmpty()) return 0; + var trimmed = s.trim(); + if (trimmed.isEmpty()) return 0; + try { + return Long.parseLong(trimmed, 8); + } catch (NumberFormatException e) { + return 0; + } + } + + @Override + public void close() throws IOException { + if (closed) return; + closed = true; + in.close(); + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/lib/archive/TarWriter.java b/app/src/main/java/cn/classfun/droidvm/lib/archive/TarWriter.java new file mode 100644 index 000000000..c47f01209 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/lib/archive/TarWriter.java @@ -0,0 +1,183 @@ +package cn.classfun.droidvm.lib.archive; + +import static java.nio.charset.StandardCharsets.US_ASCII; +import static java.nio.charset.StandardCharsets.UTF_8; + +import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.github.luben.zstd.ZstdInputStream; +import com.github.luben.zstd.ZstdOutputStream; + +import org.tukaani.xz.LZMA2Options; +import org.tukaani.xz.XZInputStream; +import org.tukaani.xz.XZOutputStream; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +public final class TarWriter implements AutoCloseable { + private static final int BLOCK = 512; + private final OutputStream out; + private boolean closed = false; + + public TarWriter(@NonNull OutputStream out) { + this.out = out; + } + + public void entry( + @NonNull String name, + byte[] content + ) throws IOException { + entry(name, content, 0, content.length); + } + + public void entry( + @NonNull String name, + byte[] content, + int off, + int len + ) throws IOException { + entry(name, len, os -> os.write(content, off, len)); + } + + public interface ContentWriter { + void write(@NonNull OutputStream os) throws IOException; + } + + @SuppressWarnings("OctalInteger") + public static final class TarMetadata { + public int mode = 0644; + public int uid = 0; + public int gid = 0; + public long mtime = System.currentTimeMillis() / 1000; + } + + public void entry( + @NonNull String name, + long size, + @NonNull ContentWriter writer + ) throws IOException { + entry(name, size, null, writer); + } + + public void entry( + @NonNull String name, + long size, + @Nullable TarMetadata metadata, + @NonNull ContentWriter writer + ) throws IOException { + if (closed) throw new IOException("TarWriter closed"); + if (name.length() > 100) { + entry("./@LongLink", name.getBytes(UTF_8)); + int slash = name.lastIndexOf('/'); + if (slash >= 0 && slash < name.length() - 1) + name = name.substring(slash + 1); + } + writeHeader(name, size, '0', metadata); + long remaining; + try (var sink = new OutputStreamLimiter(out, size)) { + writer.write(sink); + remaining = sink.getRemaining(); + } + if (remaining > 0) throw new IOException(fmt( + "entry %s: short write (%d of %d bytes)", + name, size - remaining, size + )); + int pad = (int) ((BLOCK - (size % BLOCK)) % BLOCK); + if (pad > 0) { + out.write(new byte[pad]); + } + } + + private void writeHeader( + @NonNull String name, + long size, + char type, + @Nullable TarMetadata metadata + ) throws IOException { + var hdr = new byte[BLOCK]; + var nameBytes = name.getBytes(UTF_8); + System.arraycopy(nameBytes, 0, hdr, 0, Math.min(nameBytes.length, 100)); + if (metadata == null) metadata = new TarMetadata(); + field(hdr, 100, 8, padOctal(metadata.mode, 7)); + field(hdr, 108, 8, padOctal(metadata.uid, 7)); + field(hdr, 116, 8, padOctal(metadata.gid, 7)); + field(hdr, 124, 12, padOctal(size, 11)); + field(hdr, 136, 12, padOctal(metadata.mtime, 11)); + for (int i = 148; i < 156; i++) hdr[i] = ' '; + hdr[156] = (byte) type; + var magic = "ustar"; + System.arraycopy(magic.getBytes(US_ASCII), 0, hdr, 257, 5); + hdr[262] = 0; + int sum = 0; + for (byte b : hdr) sum += b & 0xff; + field(hdr, 148, 8, Long.toOctalString(sum).concat(" ")); + out.write(hdr); + } + + private static void field(byte[] hdr, int off, int len, @NonNull String oct) { + var b = oct.getBytes(US_ASCII); + int n = Math.min(b.length, len); + System.arraycopy(b, 0, hdr, off, n); + for (int i = off + n; i < off + len; i++) hdr[i] = 0; + } + + @NonNull + private static String padOctal(long value, int digits) { + var s = Long.toOctalString(value); + if (s.length() < digits) + s = "0".repeat(digits - s.length()).concat(s); + return s; + } + + @Override + public void close() throws IOException { + if (closed) return; + closed = true; + out.write(new byte[BLOCK * 2]); + } + + @NonNull + public static OutputStream wrapCompressionOutput( + @NonNull OutputStream out, + @NonNull Compression c + ) throws Exception { + switch (c) { + case NONE: + return out; + case GZIP: + return new GZIPOutputStream(out, 64 * 1024); + case XZ: + return new XZOutputStream(out, new LZMA2Options()); + case ZSTD: + return new ZstdOutputStream(out, 3); + default: + throw new IllegalArgumentException(fmt("unknown compression: %s", c)); + } + } + + @NonNull + public static InputStream wrapCompressionInput( + @NonNull InputStream out, + @NonNull Compression c + ) throws Exception { + switch (c) { + case NONE: + return out; + case GZIP: + return new GZIPInputStream(out, 64 * 1024); + case XZ: + return new XZInputStream(out); + case ZSTD: + return new ZstdInputStream(out); + default: + throw new IllegalArgumentException(fmt("unknown compression: %s", c)); + } + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6f7857bd2..b93c2b823 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,6 +14,7 @@ okhttp3 = "5.3.2" snakeyaml = "2.6" termux = "v0.118.3" xz = "1.10" +zstd = "1.5.7-11" [libraries] activity = { group = "androidx.activity", name = "activity", version.ref = "activity" } @@ -32,6 +33,7 @@ material = { group = "com.google.android.material", name = "material", version.r okhttp3 = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp3" } snakeyaml = { group = "org.yaml", name = "snakeyaml", version.ref = "snakeyaml" } xz = { group = "org.tukaani", name = "xz", version.ref = "xz" } +zstd = { group = "com.github.luben", name = "zstd-jni", version.ref = "zstd" } #noinspection Aligned16KB termux-emulator = { group = "com.github.termux.termux-app", name = "terminal-emulator", version.ref = "termux" } termux-view = { group = "com.github.termux.termux-app", name = "terminal-view", version.ref = "termux" } From d6245c6f98138edc2014bcd2e59c3a1161086375 Mon Sep 17 00:00:00 2001 From: BigfootACA Date: Tue, 7 Jul 2026 15:25:05 +0800 Subject: [PATCH 110/182] AssetUtils: extractTarXz: refactor use TarReader Signed-off-by: BigfootACA --- .../droidvm/lib/utils/AssetUtils.java | 118 +++--------------- 1 file changed, 15 insertions(+), 103 deletions(-) diff --git a/app/src/main/java/cn/classfun/droidvm/lib/utils/AssetUtils.java b/app/src/main/java/cn/classfun/droidvm/lib/utils/AssetUtils.java index 9a6afaebf..6ca1f30f1 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/utils/AssetUtils.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/utils/AssetUtils.java @@ -24,9 +24,9 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import cn.classfun.droidvm.lib.archive.TarReader; import cn.classfun.droidvm.lib.crypt.HashFile; public final class AssetUtils { @@ -326,120 +326,32 @@ public static void extractPrebuilt( } } - // ---- minimal tar.xz extraction (USTAR; no PAX/GNU records, no symlinks) ---- - // The packer (auto-build.py) forces USTAR_FORMAT over a short-path, - // symlink-free tree, so this reader stays small; corruption is still caught - // by the per-file sha256 manifest check in needsExtractPrebuilt(). - @SuppressLint("SetWorldReadable") - @SuppressWarnings("ResultOfMethodCallIgnored") + @SuppressWarnings({"ResultOfMethodCallIgnored", "OctalInteger"}) private static void extractTarXz(@NonNull InputStream rawIn, @NonNull File destRoot) throws IOException { - try (var xz = new XZInputStream(new BufferedInputStream(rawIn))) { - var header = new byte[512]; - while (readBlock(xz, header)) { - if (isZeroBlock(header)) break; - var name = cString(header, 0, 100); - var prefix = cString(header, 345, 155); - var path = prefix.isEmpty() ? name : pathJoin(prefix, name); - var size = parseOctal(header, 124, 12); - var mode = (int) parseOctal(header, 100, 8); - var type = (char) (header[156] & 0xff); - var outFile = new File(destRoot, path); - if (type == '5') { // directory - if (!outFile.isDirectory() && !outFile.mkdirs()) - throw new IOException(fmt("Failed to mkdir %s", outFile)); - continue; - } - if (type != '0' && type != '\0') { // unexpected (symlink/etc.) - Log.w(TAG, fmt("Skipping unsupported tar entry %s (type %c)", path, type)); - skip(xz, size + padding(size)); - continue; - } + try ( + var xz = new XZInputStream(new BufferedInputStream(rawIn)); + var tar = new TarReader(xz) + ) { + tar.forEach((name, mode, size, content) -> { + var outFile = new File(destRoot, name); var parent = outFile.getParentFile(); if (parent != null && !parent.isDirectory() && !parent.mkdirs()) throw new IOException(fmt("Failed to mkdir %s", parent)); - // Unlink any existing file first. Opening it in place would fail - // with ETXTBSY if it's a currently-running executable (e.g. the - // gvswitch daemon); unlinking is safe -- a running process keeps - // the old inode while the name is rebound to the new file. if (outFile.exists() && !outFile.delete()) Log.w(TAG, fmt("Could not unlink existing %s before extract", outFile)); try (var os = new FileOutputStream(outFile)) { - copyN(xz, os, size); + copyStream(content, os); } - skip(xz, padding(size)); outFile.setReadable(true, false); - //noinspection OctalInteger - if ((mode & 0111) != 0 || path.startsWith("bin/") || path.startsWith("usr/bin/")) + if ((mode & 0111) != 0 || name.startsWith("bin/") || name.startsWith("usr/bin/")) outFile.setExecutable(true, false); - } - } - } - - private static long padding(long size) { - var rem = size % 512; - return rem == 0 ? 0 : 512 - rem; - } - - /** Read exactly one 512-byte block; false on a clean EOF between blocks. */ - private static boolean readBlock(@NonNull InputStream in, @NonNull byte[] buf) - throws IOException { - var off = 0; - while (off < buf.length) { - var n = in.read(buf, off, buf.length - off); - if (n < 0) { - if (off == 0) return false; - throw new IOException("Truncated tar stream"); - } - off += n; - } - return true; - } - - private static boolean isZeroBlock(@NonNull byte[] buf) { - for (var b : buf) if (b != 0) return false; - return true; - } - - private static String cString(@NonNull byte[] buf, int off, int len) { - var end = off; - var max = off + len; - while (end < max && buf[end] != 0) end++; - return new String(buf, off, end - off, StandardCharsets.UTF_8); - } - - private static long parseOctal(@NonNull byte[] buf, int off, int len) { - long value = 0; - var i = off; - var max = off + len; - while (i < max && (buf[i] == ' ' || buf[i] == 0)) i++; - while (i < max && buf[i] >= '0' && buf[i] <= '7') { - value = (value << 3) + (buf[i] - '0'); - i++; - } - return value; - } - - private static void copyN(@NonNull InputStream in, @NonNull FileOutputStream out, long n) - throws IOException { - var buf = new byte[8192]; - var remaining = n; - while (remaining > 0) { - var got = in.read(buf, 0, (int) Math.min(buf.length, remaining)); - if (got < 0) throw new IOException("Truncated tar entry"); - out.write(buf, 0, got); - remaining -= got; - } - } - - private static void skip(@NonNull InputStream in, long n) throws IOException { - var remaining = n; - var buf = new byte[8192]; - while (remaining > 0) { - var got = in.read(buf, 0, (int) Math.min(buf.length, remaining)); - if (got < 0) throw new IOException("Truncated tar stream"); - remaining -= got; + }); + } catch (IOException e) { + throw e; + } catch (Exception e) { + throw new IOException("Failed to extract tar.xz", e); } } From 65753b0a1b372cc5c104f8613f6befebdb6f065f Mon Sep 17 00:00:00 2001 From: BigfootACA Date: Tue, 7 Jul 2026 15:26:38 +0800 Subject: [PATCH 111/182] daemon: add command vm_get Signed-off-by: BigfootACA --- .../droidvm/daemon/ipc/vm/GetHandler.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/GetHandler.java diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/GetHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/GetHandler.java new file mode 100644 index 000000000..44be4febf --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/GetHandler.java @@ -0,0 +1,34 @@ +package cn.classfun.droidvm.daemon.ipc.vm; + +import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; + +import androidx.annotation.NonNull; + +import com.google.auto.service.AutoService; + +import cn.classfun.droidvm.daemon.server.ClientRequest; +import cn.classfun.droidvm.daemon.server.RequestException; +import cn.classfun.droidvm.daemon.server.RequestHandler; + +@AutoService(RequestHandler.class) +public final class GetHandler extends RequestHandler { + @NonNull + @Override + public String getName() { + return "vm_get"; + } + + @Override + public void handle(@NonNull ClientRequest request) throws Exception { + var params = request.getParams(); + var vmId = params.optString("vm_id", ""); + if (vmId.isEmpty()) + throw new RequestException("missing vm_id"); + var vms = request.getContext().getVMs(); + var inst = vms.findById(vmId); + if (inst == null) + throw new RequestException(fmt("VM not found: %s", vmId)); + var res = request.res(); + res.put("data", inst.toJson()); + } +} From de60fd5a068623e344bba1b35828fa169adcaaa5 Mon Sep 17 00:00:00 2001 From: BigfootACA Date: Tue, 7 Jul 2026 15:35:26 +0800 Subject: [PATCH 112/182] lib: add pkg for vm package Signed-off-by: BigfootACA --- .../cn/classfun/droidvm/lib/pkg/BootFile.java | 61 +++++++++++ .../classfun/droidvm/lib/pkg/DiskEntry.java | 75 +++++++++++++ .../cn/classfun/droidvm/lib/pkg/DiskRef.java | 54 ++++++++++ .../droidvm/lib/pkg/PackageConstants.java | 17 +++ .../droidvm/lib/pkg/PackageHeader.java | 69 ++++++++++++ .../droidvm/lib/pkg/PackageInput.java | 74 +++++++++++++ .../droidvm/lib/pkg/PackageManifest.java | 100 ++++++++++++++++++ .../cn/classfun/droidvm/lib/pkg/Phase.java | 9 ++ 8 files changed, 459 insertions(+) create mode 100644 app/src/main/java/cn/classfun/droidvm/lib/pkg/BootFile.java create mode 100644 app/src/main/java/cn/classfun/droidvm/lib/pkg/DiskEntry.java create mode 100644 app/src/main/java/cn/classfun/droidvm/lib/pkg/DiskRef.java create mode 100644 app/src/main/java/cn/classfun/droidvm/lib/pkg/PackageConstants.java create mode 100644 app/src/main/java/cn/classfun/droidvm/lib/pkg/PackageHeader.java create mode 100644 app/src/main/java/cn/classfun/droidvm/lib/pkg/PackageInput.java create mode 100644 app/src/main/java/cn/classfun/droidvm/lib/pkg/PackageManifest.java create mode 100644 app/src/main/java/cn/classfun/droidvm/lib/pkg/Phase.java diff --git a/app/src/main/java/cn/classfun/droidvm/lib/pkg/BootFile.java b/app/src/main/java/cn/classfun/droidvm/lib/pkg/BootFile.java new file mode 100644 index 000000000..70ece34ec --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/lib/pkg/BootFile.java @@ -0,0 +1,61 @@ +package cn.classfun.droidvm.lib.pkg; + +import androidx.annotation.NonNull; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; + +import cn.classfun.droidvm.lib.store.base.JSONSerialize; + +public final class BootFile implements JSONSerialize { + public String archivePath = null; + public String name = null; + public String path = null; + public String kind = null; + public long size = 0; + public File target = null; + + public BootFile() { + } + + public BootFile(@NonNull JSONObject o) { + archivePath = o.optString("archive_path"); + name = o.optString("name"); + path = o.optString("path"); + kind = o.optString("kind"); + size = o.optLong("size"); + } + + @NonNull + @Override + public JSONObject toJson() throws JSONException { + var b = new JSONObject(); + b.put("archive_path", archivePath); + b.put("name", name); + b.put("path", path); + b.put("kind", kind); + b.put("size", size); + return b; + } + + public boolean isExists() { + var f = new File(path); + return f.exists() && f.isFile(); + } + + public void build() { + var f = new File(path); + name = f.getName(); + archivePath = name; + size = f.length(); + } + + @NonNull + public static BootFile from(Object o) throws JSONException { + if (o instanceof JSONObject) + return new BootFile((JSONObject) o); + throw new JSONException("object is not json"); + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/lib/pkg/DiskEntry.java b/app/src/main/java/cn/classfun/droidvm/lib/pkg/DiskEntry.java new file mode 100644 index 000000000..0017d0e6c --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/lib/pkg/DiskEntry.java @@ -0,0 +1,75 @@ +package cn.classfun.droidvm.lib.pkg; + +import static cn.classfun.droidvm.lib.store.enums.Enums.optEnum; + +import androidx.annotation.NonNull; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; + +import cn.classfun.droidvm.lib.store.base.JSONSerialize; +import cn.classfun.droidvm.ui.disk.create.DiskFormat; + +public final class DiskEntry implements JSONSerialize { + public DiskRef ref; + public String name = null; + public DiskFormat format = null; + public long size = 0; + public String archivePath = null; + public File target = null; + + public DiskEntry(DiskRef ref) { + this.ref = ref; + } + + public DiskEntry(@NonNull JSONObject o) { + ref = new DiskRef(0, o); + name = o.optString("name", "disk.img"); + format = optEnum(o, "format", DiskFormat.RAW); + size = o.optLong("size"); + archivePath = o.optString("archive_path"); + } + + @NonNull + @Override + public JSONObject toJson() throws JSONException { + var o = ref.toJson(); + if (target != null) { + o.put("path", target.getPath()); + o.put("name", target.getName()); + } else { + o.put("name", name); + } + o.put("format", format.name().toLowerCase()); + o.put("size", size); + o.put("archive_path", archivePath); + return o; + } + + private static @NonNull String sanitize(@NonNull String name) { + var sb = new StringBuilder(); + for (int i = 0; i < name.length(); i++) { + char c = name.charAt(i); + if (c == '/' || c == '\\' || c < 0x20) c = '_'; + sb.append(c); + } + return sb.toString(); + } + + public void build() { + var file = new File(ref.path); + name = file.getName(); + archivePath = sanitize(name); + format = DiskFormat.fromFilename(name); + size = file.length(); + } + + @NonNull + public static DiskEntry from(Object o) throws JSONException { + if (o instanceof JSONObject) + return new DiskEntry((JSONObject) o); + throw new JSONException("object is not json"); + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/lib/pkg/DiskRef.java b/app/src/main/java/cn/classfun/droidvm/lib/pkg/DiskRef.java new file mode 100644 index 000000000..768b98763 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/lib/pkg/DiskRef.java @@ -0,0 +1,54 @@ +package cn.classfun.droidvm.lib.pkg; + +import static java.util.Objects.requireNonNull; +import static cn.classfun.droidvm.lib.store.enums.Enums.optEnum; + +import androidx.annotation.NonNull; + +import org.json.JSONException; +import org.json.JSONObject; + +import cn.classfun.droidvm.lib.store.base.DataItem; +import cn.classfun.droidvm.lib.store.base.JSONSerialize; +import cn.classfun.droidvm.lib.store.disk.DiskBus; +import cn.classfun.droidvm.ui.disk.create.DiskFormat; + +public final class DiskRef implements JSONSerialize { + public int index; + public String path; + public boolean readonly; + public DiskBus bus; + + public DiskRef(int index, @NonNull JSONObject jo) { + this.index = index; + this.path = jo.optString("path", ""); + this.readonly = jo.optBoolean("readonly", false); + this.bus = optEnum(jo, "bus", DiskBus.VIRTIO); + } + + public DiskRef(int index, @NonNull DataItem o) { + this.index = index; + this.path = o.optString("path", ""); + this.readonly = o.optBoolean("readonly", false); + this.bus = optEnum(o, "bus", DiskBus.VIRTIO); + } + + @NonNull + public DiskFormat getFormat() { + return DiskFormat.fromFilename(requireNonNull(path)); + } + + public boolean isCDROM() { + return getFormat() == DiskFormat.ISO; + } + + @NonNull + @Override + public JSONObject toJson() throws JSONException { + var d = new JSONObject(); + d.put("readonly", readonly); + d.put("bus", bus); + d.put("path", path); + return d; + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/lib/pkg/PackageConstants.java b/app/src/main/java/cn/classfun/droidvm/lib/pkg/PackageConstants.java new file mode 100644 index 000000000..bca0dadfd --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/lib/pkg/PackageConstants.java @@ -0,0 +1,17 @@ +package cn.classfun.droidvm.lib.pkg; + +import cn.classfun.droidvm.lib.archive.Compression; + +public final class PackageConstants { + public static final String EXTENSION = "vmpkg"; + public static final String MIME = "application/vnd.droidvm.vmpkg"; + public static final String MAGIC = "VMPKG"; + public static final int HEADER_SIZE = 24; + public static final int BUFFER = 64 * 1024; + public static final int MANIFEST_VERSION = 1; + public static final String MANIFEST_NAME = "manifest.json"; + public static final Compression DEFAULT_COMPRESSION = Compression.ZSTD; + + private PackageConstants() { + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/lib/pkg/PackageHeader.java b/app/src/main/java/cn/classfun/droidvm/lib/pkg/PackageHeader.java new file mode 100644 index 000000000..08d934b12 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/lib/pkg/PackageHeader.java @@ -0,0 +1,69 @@ +package cn.classfun.droidvm.lib.pkg; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static cn.classfun.droidvm.lib.utils.BinaryUtils.getInt64LE; +import static cn.classfun.droidvm.lib.utils.BinaryUtils.getUInt16LE; +import static cn.classfun.droidvm.lib.utils.BinaryUtils.readFully; +import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; + +import androidx.annotation.NonNull; + +import java.io.IOException; +import java.io.InputStream; + +import cn.classfun.droidvm.lib.archive.Compression; + +public final class PackageHeader { + public int manifestVersion = 0; + public int appVersionCode = 0; + public int manifestSize = 0; + public int compression = 0; + public long dataSize = 0; + + public PackageHeader() { + } + + public void parseFromData(@NonNull byte[] hdr) throws IOException { + if (hdr.length < PackageConstants.HEADER_SIZE) + throw new IOException("incomplete package header"); + if (!checkMagic(hdr)) + throw new IOException("invalid package header"); + manifestVersion = getUInt16LE(hdr, 6); + appVersionCode = getUInt16LE(hdr, 8); + manifestSize = getUInt16LE(hdr, 10); + compression = getUInt16LE(hdr, 12); + dataSize = getInt64LE(hdr, 16); + } + + public void validate() throws IOException { + if (manifestVersion != PackageConstants.MANIFEST_VERSION) + throw new IOException(fmt("unsupported vmpkg manifest version: %d", manifestVersion)); + if (Compression.fromType(compression) == null) + throw new IOException(fmt("unsupported vmpkg compression: %d", compression)); + if (manifestSize <= 0) throw new IOException("empty vmpkg manifest"); + if (dataSize < 0) throw new IOException("vmpkg data size is too large"); + } + + @NonNull + public static PackageHeader fromBytes(byte[] hdr) throws IOException { + var header = new PackageHeader(); + header.parseFromData(hdr); + header.validate(); + return header; + } + + @NonNull + public static PackageHeader fromStream(InputStream in) throws IOException { + var hdr = new byte[PackageConstants.HEADER_SIZE]; + readFully(in, hdr); + return fromBytes(hdr); + } + + public static boolean checkMagic(@NonNull byte[] hdr) { + if (hdr.length < PackageConstants.MAGIC.length() + 1) return false; + var magic = PackageConstants.MAGIC.getBytes(UTF_8); + for (int i = 0; i < magic.length; i++) + if (hdr[i] != magic[i]) return false; + return hdr[magic.length] == 0; + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/lib/pkg/PackageInput.java b/app/src/main/java/cn/classfun/droidvm/lib/pkg/PackageInput.java new file mode 100644 index 000000000..7a35024ad --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/lib/pkg/PackageInput.java @@ -0,0 +1,74 @@ +package cn.classfun.droidvm.lib.pkg; + +import static cn.classfun.droidvm.lib.archive.TarWriter.wrapCompressionInput; +import static cn.classfun.droidvm.lib.utils.BinaryUtils.alignUp; +import static cn.classfun.droidvm.lib.utils.BinaryUtils.alignUpStrict; +import static cn.classfun.droidvm.lib.utils.BinaryUtils.readZeroPadding; + +import androidx.annotation.NonNull; + +import java.io.IOException; +import java.io.InputStream; + +import cn.classfun.droidvm.lib.archive.Compression; +import cn.classfun.droidvm.lib.archive.LimitedInputStream; + +public final class PackageInput implements AutoCloseable { + @NonNull + private final InputStream source; + @NonNull + private final LimitedInputStream rawData; + @NonNull + public final InputStream data; + @NonNull + public final PackageManifest manifest; + @NonNull + private final PackageHeader header; + + public PackageInput( + @NonNull InputStream source, + @NonNull LimitedInputStream rawData, + @NonNull InputStream data, + @NonNull PackageManifest manifest, + @NonNull PackageHeader header + ) { + this.source = source; + this.rawData = rawData; + this.data = data; + this.manifest = manifest; + this.header = header; + } + + public void validateDataConsumed() throws IOException { + if (rawData.getRemaining() != 0) + throw new IOException("vmpkg data_size is larger than tarball data"); + long dataEnd = alignUpStrict(alignUp(PackageConstants.HEADER_SIZE) + header.manifestSize); + dataEnd += header.dataSize; + readZeroPadding(source, alignUp(dataEnd) - dataEnd); + if (source.read() >= 0) throw new IOException("unexpected bytes after vmpkg package"); + } + + @Override + public void close() throws IOException { + data.close(); + } + + @NonNull + public static PackageInput open(@NonNull InputStream raw) throws Exception { + var pkgHeader = PackageHeader.fromStream(raw); + var manifest = PackageManifest.fromStream(raw, pkgHeader); + if (pkgHeader.manifestVersion != manifest.manifestVersion) + throw new IOException("vmpkg manifest_version mismatch"); + if (pkgHeader.appVersionCode != manifest.appVersionCode) + throw new IOException("vmpkg app_version_code mismatch"); + if (pkgHeader.compression != manifest.compression.type) + throw new IOException("vmpkg compression mismatch"); + long pos = alignUp(PackageConstants.HEADER_SIZE) + pkgHeader.manifestSize; + readZeroPadding(raw, alignUpStrict(pos) - pos); + var stream = new LimitedInputStream(raw, pkgHeader.dataSize); + var compression = Compression.fromType(pkgHeader.compression); + if (compression == null) throw new IllegalArgumentException("invalid compression"); + var compStream = wrapCompressionInput(stream, compression); + return new PackageInput(raw, stream, compStream, manifest, pkgHeader); + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/lib/pkg/PackageManifest.java b/app/src/main/java/cn/classfun/droidvm/lib/pkg/PackageManifest.java new file mode 100644 index 000000000..eaa900961 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/lib/pkg/PackageManifest.java @@ -0,0 +1,100 @@ +package cn.classfun.droidvm.lib.pkg; + +import static cn.classfun.droidvm.lib.store.enums.Enums.optEnum; +import static cn.classfun.droidvm.lib.utils.BinaryUtils.alignUp; +import static cn.classfun.droidvm.lib.utils.BinaryUtils.decodeUtf8; +import static cn.classfun.droidvm.lib.utils.BinaryUtils.readFully; +import static cn.classfun.droidvm.lib.utils.BinaryUtils.readZeroPadding; +import static cn.classfun.droidvm.lib.utils.JsonUtils.arrayToList; +import static cn.classfun.droidvm.lib.utils.JsonUtils.listToJSONArray; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import cn.classfun.droidvm.BuildConfig; +import cn.classfun.droidvm.lib.archive.Compression; +import cn.classfun.droidvm.lib.store.base.JSONSerialize; +import cn.classfun.droidvm.lib.store.network.NetworkConfig; +import cn.classfun.droidvm.lib.store.vm.VMConfig; + +public final class PackageManifest implements JSONSerialize { + public int manifestVersion = PackageConstants.MANIFEST_VERSION; + public String format = PackageConstants.EXTENSION; + public long createdAt = System.currentTimeMillis(); + public String appVersion = BuildConfig.VERSION_NAME; + public int appVersionCode = BuildConfig.VERSION_CODE; + public String appBuildType = BuildConfig.BUILD_TYPE; + public Compression compression = PackageConstants.DEFAULT_COMPRESSION; + public VMConfig vm = new VMConfig(); + public List disks = new ArrayList<>(); + public List boots = new ArrayList<>(); + public List networks = new ArrayList<>(); + + @NonNull + @Override + public JSONObject toJson() throws JSONException{ + var o = new JSONObject(); + o.put("manifest_version", manifestVersion); + o.put("format", format); + o.put("created_at", createdAt); + o.put("app_version", appVersion); + o.put("app_version_code", appVersionCode); + o.put("app_build_type", appBuildType); + o.put("compression", compression); + o.put("vm", vm.toJson()); + o.put("disks", listToJSONArray(disks)); + o.put("boots", listToJSONArray(boots)); + o.put("networks", listToJSONArray(networks)); + return o; + } + + @Nullable + public DiskEntry findDisk(@NonNull String archivePath) { + for (var disk : disks) + if (archivePath.equals(disk.archivePath)) return disk; + return null; + } + + @Nullable + public BootFile findBoot(@NonNull String archivePath) { + for (var boot : boots) + if (archivePath.equals(boot.archivePath)) return boot; + return null; + } + + @NonNull + public static PackageManifest fromStream( + @NonNull InputStream in, + @NonNull PackageHeader hdr + ) throws Exception { + readZeroPadding(in, alignUp(PackageConstants.HEADER_SIZE) - PackageConstants.HEADER_SIZE); + var manifest = new byte[hdr.manifestSize]; + readFully(in, manifest); + var json = new JSONObject(decodeUtf8(manifest)); + return fromJson(json); + } + + @NonNull + public static PackageManifest fromJson(@NonNull JSONObject o) throws JSONException { + var m = new PackageManifest(); + m.manifestVersion = o.optInt("manifest_version"); + m.format = o.optString("format"); + m.createdAt = o.optLong("created_at"); + m.appVersion = o.optString("app_version"); + m.appVersionCode = o.optInt("app_version_code"); + m.appBuildType = o.optString("app_build_type"); + m.compression = optEnum(o, "compression", PackageConstants.DEFAULT_COMPRESSION); + m.vm = new VMConfig(o.getJSONObject("vm")); + m.disks = arrayToList(o, "disks", DiskEntry::from); + m.boots = arrayToList(o, "boots", BootFile::from); + m.networks = arrayToList(o, "networks", (JSONObject net) -> new NetworkConfig(net)); + return m; + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/lib/pkg/Phase.java b/app/src/main/java/cn/classfun/droidvm/lib/pkg/Phase.java new file mode 100644 index 000000000..41646fe3b --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/lib/pkg/Phase.java @@ -0,0 +1,9 @@ +package cn.classfun.droidvm.lib.pkg; + +public enum Phase { + IDLE, + SCAN, + PACK, + DONE, + ERROR +} From 9e50164cb7fbb66813f677a795b0d074a58931ac Mon Sep 17 00:00:00 2001 From: BigfootACA Date: Tue, 7 Jul 2026 15:36:21 +0800 Subject: [PATCH 113/182] daemon: add vm_export and vm_import for vm package Signed-off-by: BigfootACA --- .../droidvm/daemon/ipc/vm/ExportHandler.java | 35 +++ .../droidvm/daemon/ipc/vm/ImportHandler.java | 34 ++ .../droidvm/daemon/server/ServerContext.java | 17 + .../droidvm/daemon/vm/pkg/VMExportTask.java | 220 +++++++++++++ .../droidvm/daemon/vm/pkg/VMExportUtils.java | 172 +++++++++++ .../droidvm/daemon/vm/pkg/VMImportTask.java | 292 ++++++++++++++++++ .../droidvm/daemon/vm/pkg/VMImportUtils.java | 67 ++++ 7 files changed, 837 insertions(+) create mode 100644 app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ExportHandler.java create mode 100644 app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ImportHandler.java create mode 100644 app/src/main/java/cn/classfun/droidvm/daemon/vm/pkg/VMExportTask.java create mode 100644 app/src/main/java/cn/classfun/droidvm/daemon/vm/pkg/VMExportUtils.java create mode 100644 app/src/main/java/cn/classfun/droidvm/daemon/vm/pkg/VMImportTask.java create mode 100644 app/src/main/java/cn/classfun/droidvm/daemon/vm/pkg/VMImportUtils.java diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ExportHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ExportHandler.java new file mode 100644 index 000000000..28cf88ea9 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ExportHandler.java @@ -0,0 +1,35 @@ +package cn.classfun.droidvm.daemon.ipc.vm; + +import androidx.annotation.NonNull; + +import com.google.auto.service.AutoService; + +import cn.classfun.droidvm.daemon.server.ClientRequest; +import cn.classfun.droidvm.daemon.server.RequestException; +import cn.classfun.droidvm.daemon.server.RequestHandler; +import cn.classfun.droidvm.daemon.vm.pkg.VMExportTask; + +@AutoService(RequestHandler.class) +public final class ExportHandler extends RequestHandler { + @NonNull + @Override + public String getName() { + return "vm_export"; + } + + @Override + public void handle(@NonNull ClientRequest request) throws Exception { + var params = request.getParams(); + var destPath = params.optString("dest_path", ""); + if (!params.has("vm_id")) + throw new RequestException("missing vm_id"); + if (destPath.isEmpty()) + throw new RequestException("missing dest_path"); + var store = request.getContext().getExportTaskStore(); + var server = request.getClient().getServer(); + var task = new VMExportTask(server, params); + store.put(task.taskId, task); + task.startAsync(); + request.res().put("task_id", task.taskId.toString()); + } +} \ No newline at end of file diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ImportHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ImportHandler.java new file mode 100644 index 000000000..caac7fe4f --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ImportHandler.java @@ -0,0 +1,34 @@ +package cn.classfun.droidvm.daemon.ipc.vm; + +import androidx.annotation.NonNull; + +import com.google.auto.service.AutoService; + +import cn.classfun.droidvm.daemon.server.ClientRequest; +import cn.classfun.droidvm.daemon.server.RequestException; +import cn.classfun.droidvm.daemon.server.RequestHandler; +import cn.classfun.droidvm.daemon.vm.pkg.VMImportTask; + +@AutoService(RequestHandler.class) +public final class ImportHandler extends RequestHandler { + @NonNull + @Override + public String getName() { + return "vm_import"; + } + + @Override + public void handle(@NonNull ClientRequest request) throws Exception { + var params = request.getParams(); + if (params.optString("src_path", "").isEmpty()) + throw new RequestException("missing src_path"); + if (params.optString("target_dir", "").isEmpty()) + throw new RequestException("missing target_dir"); + var store = request.getContext().getImportTaskStore(); + var server = request.getClient().getServer(); + var task = new VMImportTask(server, params); + store.put(task.taskId, task); + task.startAsync(); + request.res().put("task_id", task.taskId.toString()); + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/server/ServerContext.java b/app/src/main/java/cn/classfun/droidvm/daemon/server/ServerContext.java index 043c2cb60..30607aab8 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/server/ServerContext.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/server/ServerContext.java @@ -11,10 +11,15 @@ import androidx.annotation.NonNull; import java.io.File; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import cn.classfun.droidvm.daemon.network.NetworkInstanceStore; import cn.classfun.droidvm.daemon.network.backend.DefaultRouterWatcher; import cn.classfun.droidvm.daemon.vm.VMInstanceStore; +import cn.classfun.droidvm.daemon.vm.pkg.VMExportTask; +import cn.classfun.droidvm.daemon.vm.pkg.VMImportTask; import cn.classfun.droidvm.lib.store.base.DataItem; public final class ServerContext { @@ -22,6 +27,8 @@ public final class ServerContext { private final VMInstanceStore vms = new VMInstanceStore(this); private final NetworkInstanceStore networks = new NetworkInstanceStore(this); private final DefaultRouterWatcher routerWatcher = new DefaultRouterWatcher(this); + private final Map exportTasks = new ConcurrentHashMap<>(); + private final Map importTasks = new ConcurrentHashMap<>(); public DataItem appConfig = DataItem.newObject(); public ServerContext() { @@ -77,4 +84,14 @@ public NetworkInstanceStore getNetworks() { public DefaultRouterWatcher getRouterWatcher() { return routerWatcher; } + + @NonNull + public Map getExportTaskStore() { + return exportTasks; + } + + @NonNull + public Map getImportTaskStore() { + return importTasks; + } } diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/pkg/VMExportTask.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/pkg/VMExportTask.java new file mode 100644 index 000000000..b40674956 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/pkg/VMExportTask.java @@ -0,0 +1,220 @@ +package cn.classfun.droidvm.daemon.vm.pkg; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static cn.classfun.droidvm.daemon.vm.pkg.VMExportUtils.buildHeader; +import static cn.classfun.droidvm.daemon.vm.pkg.VMExportUtils.collectBootFiles; +import static cn.classfun.droidvm.daemon.vm.pkg.VMExportUtils.collectNetworks; +import static cn.classfun.droidvm.daemon.vm.pkg.VMExportUtils.sanitizeVM; +import static cn.classfun.droidvm.lib.archive.TarWriter.wrapCompressionOutput; +import static cn.classfun.droidvm.lib.pkg.PackageConstants.BUFFER; +import static cn.classfun.droidvm.lib.store.enums.Enums.optEnum; +import static cn.classfun.droidvm.lib.utils.BinaryUtils.alignUp; +import static cn.classfun.droidvm.lib.utils.BinaryUtils.alignUpStrict; +import static cn.classfun.droidvm.lib.utils.BinaryUtils.writeZero; +import static cn.classfun.droidvm.lib.utils.StringUtils.basename; +import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; +import static cn.classfun.droidvm.lib.utils.ThreadUtils.runOnPool; + +import android.util.Log; + +import androidx.annotation.NonNull; + +import org.json.JSONObject; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.util.HashSet; +import java.util.UUID; + +import cn.classfun.droidvm.daemon.server.Server; +import cn.classfun.droidvm.lib.archive.RandomAccessFileOutputStream; +import cn.classfun.droidvm.lib.archive.TarWriter; +import cn.classfun.droidvm.lib.pkg.DiskEntry; +import cn.classfun.droidvm.lib.pkg.DiskRef; +import cn.classfun.droidvm.lib.pkg.PackageConstants; +import cn.classfun.droidvm.lib.pkg.PackageManifest; +import cn.classfun.droidvm.lib.pkg.Phase; +import cn.classfun.droidvm.lib.store.base.DataItem; + +public final class VMExportTask { + private static final String TAG = "VMExportTask"; + private static final long PACK_PROGRESS_INTERVAL_MS = 500; + public final UUID taskId = UUID.randomUUID(); + private final Server server; + private final PackageManifest manifest = new PackageManifest(); + private final UUID sourceVMId; + private final int totalItems; + private final String destPath; + + public VMExportTask( + @NonNull Server server, + @NonNull JSONObject request + ) { + this.server = server; + destPath = request.optString("dest_path"); + if (!destPath.startsWith("/")) + throw new IllegalArgumentException("missing dest_path"); + var vmId = UUID.fromString(request.optString("vm_id")); + sourceVMId = vmId; + var vm = server.getContext().getVMs().findById(vmId); + if (vm == null) throw new IllegalArgumentException("target vm not found"); + manifest.vm = sanitizeVM(vm); + manifest.compression = optEnum( + request, "compression", + PackageConstants.DEFAULT_COMPRESSION + ); + var wanted = request.optJSONArray("disks"); + var wantedSet = new HashSet(); + if (wanted != null) for (int i = 0; i < wanted.length(); i++) + wantedSet.add(wanted.optInt(i, -1)); + var arr = vm.item.opt("disks", DataItem.newArray()); + for (int i = 0; i < arr.size(); i++) { + var e = arr.get(i); + if (!e.is(DataItem.Type.OBJECT)) continue; + if (!wantedSet.isEmpty() && !wantedSet.contains(i)) continue; + var disk = new DiskEntry(new DiskRef(i, e)); + disk.build(); + manifest.disks.add(disk); + } + collectBootFiles(manifest); + collectNetworks(manifest, server.getContext().getNetworks(), vm); + manifest.vm.item.remove("disks"); + totalItems = manifest.disks.size() + manifest.boots.size(); + } + + public void startAsync() { + runOnPool(() -> { + var data = DataItem.newObject(); + data.set("total", totalItems); + try { + startWrite(); + data.set("done", totalItems); + emit(data, Phase.DONE); + } catch (Exception e) { + Log.w(TAG, fmt("Export task %s failed", taskId), e); + data.set("done", 0); + data.set("message", e.getMessage()); + emit(data, Phase.ERROR); + } finally { + var store = server.getContext().getExportTaskStore(); + store.remove(taskId); + } + }); + } + + private void startWrite() throws Exception { + var outFile = new File(destPath); + var manifestBytes = manifest.toJson().toString(4).getBytes(UTF_8); + if (manifestBytes.length > 0xffff) + throw new IOException(fmt("manifest too large: %d bytes", manifestBytes.length)); + try (var raf = new RandomAccessFile(outFile, "rw")) { + raf.setLength(0); + raf.write(new byte[PackageConstants.HEADER_SIZE]); + writeZero(raf, alignUp(raf.getFilePointer()) - raf.getFilePointer()); + raf.write(manifestBytes); + writeZero(raf, alignUpStrict(raf.getFilePointer()) - raf.getFilePointer()); + long dataStart = raf.getFilePointer(); + try ( + var fileOut = new RandomAccessFileOutputStream(raf); + var compressed = wrapCompressionOutput(fileOut, manifest.compression); + var tar = new TarWriter(compressed) + ) { + tar.entry(PackageConstants.MANIFEST_NAME, manifestBytes); + pack(tar); + } + long dataEnd = raf.getFilePointer(); + long dataSize = dataEnd - dataStart; + writeZero(raf, alignUp(dataEnd) - dataEnd); + raf.seek(0); + raf.write(buildHeader(manifest, manifestBytes.length, dataSize)); + } + } + + private void pack(@NonNull TarWriter tar) throws Exception { + int done; + var data = DataItem.newObject(); + data.set("done", 0); + if (!manifest.disks.isEmpty()) { + data.set("total", manifest.disks.size()); + emit(data, Phase.PACK); + done = 0; + for (var m : manifest.disks) { + writeFile( + tar, m.archivePath, m.ref.path, + done, manifest.disks.size() + ); + done++; + } + } + if (!manifest.boots.isEmpty()) { + data.set("total", manifest.boots.size()); + emit(data, Phase.PACK); + done = 0; + for (var bf : manifest.boots) { + writeFile( + tar, bf.archivePath, bf.path, + done, manifest.boots.size() + ); + done++; + } + } + } + + private void writeFile( + @NonNull TarWriter tar, + @NonNull String item, + @NonNull String path, + int doneItems, + int totalItems + ) throws Exception { + var f = new File(path); + var size = f.length(); + var data = DataItem.newObject(); + data.set("done", doneItems); + data.set("total", totalItems); + data.set("file", basename(path)); + data.set("bytes_done", 0); + data.set("bytes_total", size); + emit(data, Phase.PACK); + tar.entry(item, size, os -> { + try (var is = new FileInputStream(f)) { + var buf = new byte[BUFFER]; + long written = 0; + long lastEmit = System.currentTimeMillis(); + int r; + while ((r = is.read(buf)) > 0) { + os.write(buf, 0, r); + written += r; + var now = System.currentTimeMillis(); + if (now - lastEmit >= PACK_PROGRESS_INTERVAL_MS) { + data.set("bytes_done", written); + emit(data, Phase.PACK); + lastEmit = now; + } + } + data.set("bytes_done", size); + data.set("done", doneItems + 1); + emit(data, Phase.PACK); + } + }); + } + + private void emit(@NonNull DataItem item, @NonNull Phase phase) { + try { + var data = item.toJson(); + data.put("event", "vm_export_status"); + data.put("task_id", taskId.toString()); + data.put("phase", phase.name().toLowerCase()); + data.put("vm_id", sourceVMId.toString()); + data.put("vm_name", manifest.vm.getName()); + var ev = new JSONObject(); + ev.put("type", "event"); + ev.put("data", data); + server.broadcastEvent(ev); + } catch (Exception e) { + Log.w(TAG, fmt("Failed to emit export event for %s", taskId), e); + } + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/pkg/VMExportUtils.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/pkg/VMExportUtils.java new file mode 100644 index 000000000..2ed8a7934 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/pkg/VMExportUtils.java @@ -0,0 +1,172 @@ +package cn.classfun.droidvm.daemon.vm.pkg; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static cn.classfun.droidvm.lib.Constants.PATH_BUILTIN_INITRD; +import static cn.classfun.droidvm.lib.Constants.PATH_BUILTIN_KERNEL; +import static cn.classfun.droidvm.lib.Constants.PATH_EDK2_FIRMWARE; +import static cn.classfun.droidvm.lib.Constants.PATH_EDK2_QEMU_FIRMWARE; +import static cn.classfun.droidvm.lib.Constants.PATH_MICRODROID_INITRD; +import static cn.classfun.droidvm.lib.Constants.PATH_MICRODROID_INITRD_DEBUG; +import static cn.classfun.droidvm.lib.Constants.PATH_MICRODROID_KERNEL; +import static cn.classfun.droidvm.lib.utils.BinaryUtils.putInt64LE; +import static cn.classfun.droidvm.lib.utils.BinaryUtils.putUInt16LE; +import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; + +import android.util.Log; + +import androidx.annotation.NonNull; + +import java.io.IOException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Set; + +import cn.classfun.droidvm.BuildConfig; +import cn.classfun.droidvm.daemon.network.NetworkInstanceStore; +import cn.classfun.droidvm.lib.pkg.BootFile; +import cn.classfun.droidvm.lib.pkg.PackageConstants; +import cn.classfun.droidvm.lib.pkg.PackageManifest; +import cn.classfun.droidvm.lib.store.base.DataItem; +import cn.classfun.droidvm.lib.store.network.NetworkConfig; +import cn.classfun.droidvm.lib.store.vm.BootConfig; +import cn.classfun.droidvm.lib.store.vm.VMConfig; + +public final class VMExportUtils { + private static final String TAG = "VMExportUtils"; + private VMExportUtils() { + } + + public static void scrubUnique(@NonNull DataItem item) { + if (item.is(DataItem.Type.OBJECT)) { + item.remove("id"); + item.remove("vm_id"); + item.remove("uuid"); + item.remove("mac"); + item.remove("mac_address"); + item.remove("tap_name"); + for (var child : item.asObject().values()) + scrubUnique(child); + } else if (item.is(DataItem.Type.ARRAY)) { + for (var child : item.asArray()) + scrubUnique(child); + } + } + + @NonNull + public static byte[] buildHeader( + @NonNull PackageManifest manifest, + int manifestSize, + long dataSize + ) throws IOException { + var hdr = new byte[PackageConstants.HEADER_SIZE]; + var magic = PackageConstants.MAGIC.getBytes(UTF_8); + System.arraycopy(magic, 0, hdr, 0, magic.length); + putUInt16LE(hdr, 6, PackageConstants.MANIFEST_VERSION); + putUInt16LE(hdr, 8, BuildConfig.VERSION_CODE); + putUInt16LE(hdr, 10, manifestSize); + putUInt16LE(hdr, 12, manifest.compression.type); + putInt64LE(hdr, 16, dataSize); + return hdr; + } + + private static void addBootFile( + @NonNull PackageManifest manifest, + @NonNull String path, + @NonNull String kind, + @NonNull Set builtins, + @NonNull Set seen + ) { + if (path.isEmpty()) return; + if (builtins.contains(path)) return; + var file = new BootFile(); + file.path = path; + file.kind = kind; + if (!file.isExists()) return; + file.build(); + if (!seen.add(file.archivePath)) return; + manifest.boots.add(file); + } + + public static void collectBootFiles(@NonNull PackageManifest manifest) { + var builtins = new HashSet(); + builtins.add(PATH_BUILTIN_KERNEL); + builtins.add(PATH_BUILTIN_INITRD); + builtins.add(PATH_EDK2_FIRMWARE); + builtins.add(PATH_EDK2_QEMU_FIRMWARE); + builtins.add(PATH_MICRODROID_KERNEL); + builtins.add(PATH_MICRODROID_INITRD); + builtins.add(PATH_MICRODROID_INITRD_DEBUG); + var seen = new LinkedHashSet(); + var boot = BootConfig.of(manifest.vm); + if (boot.getProtocol() == BootConfig.Protocol.UEFI) { + addBootFile(manifest, boot.getUefiFirmware(), "uefi", builtins, seen); + } else if (boot.getLinuxSource() == BootConfig.LinuxSource.MANUAL) { + addBootFile(manifest, boot.getKernel(), "kernel", builtins, seen); + addBootFile(manifest, boot.getInitrd(), "initrd", builtins, seen); + } + } + + @NonNull + public static VMConfig sanitizeVM(@NonNull VMConfig vm) { + try { + var copy = new VMConfig(vm.toJson()); + copy.item.remove("id"); + var nets = copy.item.opt("networks", null); + if (nets != null && nets.is(DataItem.Type.ARRAY)) { + var refMap = new HashMap(); + int n = 0; + for (var item : nets.asArray()) { + if (!item.is(DataItem.Type.OBJECT)) continue; + var original = item.optString("network_id", ""); + if (!original.isEmpty()) { + var ref = refMap.get(original); + if (ref == null) { + ref = fmt("net%d", n++); + refMap.put(original, ref); + } + item.set("pkg_network_ref", ref); + } + item.remove("network_id"); + item.remove("mac_address"); + item.remove("tap_name"); + } + } + return copy; + } catch (Exception e) { + throw new IllegalArgumentException("failed to sanitize VM config", e); + } + } + + public static void collectNetworks( + @NonNull PackageManifest manifest, + @NonNull NetworkInstanceStore store, + @NonNull VMConfig sourceVM + ) { + var srcNets = sourceVM.item.opt("networks", null); + var exportNets = manifest.vm.item.opt("networks", null); + if (srcNets == null || exportNets == null) return; + if (!srcNets.is(DataItem.Type.ARRAY) || !exportNets.is(DataItem.Type.ARRAY)) return; + var seen = new HashSet(); + var srcArr = srcNets.asArray(); + var exportArr = exportNets.asArray(); + for (int i = 0; i < srcArr.size() && i < exportArr.size(); i++) { + var src = srcArr.get(i); + var exported = exportArr.get(i); + if (!src.is(DataItem.Type.OBJECT) || !exported.is(DataItem.Type.OBJECT)) continue; + var netId = src.optString("network_id", ""); + var ref = exported.optString("pkg_network_ref", ""); + if (netId.isEmpty() || ref.isEmpty() || !seen.add(netId)) continue; + var net = store.findById(netId); + if (net == null) continue; + try { + var cfg = new NetworkConfig(net.toJson()); + scrubUnique(cfg.item); + cfg.item.set("pkg_network_ref", ref); + manifest.networks.add(cfg); + } catch (Exception e) { + Log.w(TAG, fmt("Failed to export network %s", netId), e); + } + } + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/pkg/VMImportTask.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/pkg/VMImportTask.java new file mode 100644 index 000000000..dcf56b774 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/pkg/VMImportTask.java @@ -0,0 +1,292 @@ +package cn.classfun.droidvm.daemon.vm.pkg; + +import static cn.classfun.droidvm.daemon.vm.pkg.VMImportUtils.remapBootPaths; +import static cn.classfun.droidvm.daemon.vm.pkg.VMImportUtils.remapDiskPaths; +import static cn.classfun.droidvm.daemon.vm.pkg.VMImportUtils.uniqueFile; +import static cn.classfun.droidvm.lib.pkg.PackageConstants.BUFFER; +import static cn.classfun.droidvm.lib.utils.JsonUtils.listToJSONArray; +import static cn.classfun.droidvm.lib.utils.StringUtils.basename; +import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; +import static cn.classfun.droidvm.lib.utils.ThreadUtils.runOnPool; + +import android.util.Log; + +import androidx.annotation.NonNull; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; + +import cn.classfun.droidvm.daemon.server.Server; +import cn.classfun.droidvm.lib.archive.TarReader; +import cn.classfun.droidvm.lib.pkg.BootFile; +import cn.classfun.droidvm.lib.pkg.DiskEntry; +import cn.classfun.droidvm.lib.pkg.PackageInput; +import cn.classfun.droidvm.lib.pkg.PackageManifest; +import cn.classfun.droidvm.lib.pkg.Phase; +import cn.classfun.droidvm.lib.store.base.DataItem; +import cn.classfun.droidvm.lib.store.network.NetworkConfig; +import cn.classfun.droidvm.lib.store.vm.VMConfig; + +public final class VMImportTask { + private static final String TAG = "VMImportTask"; + private static final long PROGRESS_INTERVAL_MS = 500; + public final UUID taskId = UUID.randomUUID(); + private final Server server; + private final String srcPath; + private final File targetDir; + private final String networkMode; + private int totalItems; + public VMConfig importedVM = null; + public PackageManifest importedManifest = null; + public final ArrayList placedDisks = new ArrayList<>(); + public final ArrayList placedBoots = new ArrayList<>(); + + public VMImportTask(@NonNull Server server, @NonNull JSONObject request) { + this.server = server; + srcPath = request.optString("src_path", ""); + if (!srcPath.startsWith("/")) + throw new IllegalArgumentException("missing src_path"); + var targetPath = request.optString("target_dir", ""); + if (!targetPath.startsWith("/")) + throw new IllegalArgumentException("missing target_dir"); + targetDir = new File(targetPath); + networkMode = request.optString("network_mode", "auto"); + } + + public void startAsync() { + runOnPool(this::run); + } + + private void run() { + var data = DataItem.newObject(); + try { + unpack(); + importedVM.setName(uniqueVMName(importedVM.getName())); + var networks = importNetworks(importedVM, importedManifest.networks); + var vmId = server.getContext().getVMs().createVM(importedVM); + if (vmId == null || vmId.isEmpty()) throw new IOException("failed to register VM"); + data.set("done", totalItems); + data.set("total", totalItems); + data.set("vm_id", vmId); + data.set("vm_name", importedVM.getName()); + data.set("disks", listToJSONArray(placedDisks)); + data.set("boots", listToJSONArray(placedBoots)); + data.set("networks", networks); + emit(data, Phase.DONE); + } catch (Exception e) { + Log.w(TAG, fmt("Import task %s failed", taskId), e); + data.set("done", 0); + data.set("total", totalItems); + data.set("message", e.getMessage()); + emit(data, Phase.ERROR); + } finally { + server.getContext().getImportTaskStore().remove(taskId); + } + } + + private void onTarItem( + @NonNull String name, + int mode, + long size, + @NonNull InputStream content + ) throws Exception{ + var disk = importedManifest.findDisk(name); + if (disk != null) { + var target = uniqueFile(targetDir, disk.name); + copyEntry(content, target, size, placedDisks.size(), totalItems); + disk.target = target; + placedDisks.add(disk); + return; + } + var boot = importedManifest.findBoot(name); + if (boot != null) { + var dir = new File(targetDir, "boot"); + if (!dir.exists() && !dir.mkdirs()) + throw new IOException(fmt("Cannot create %s", dir)); + var target = uniqueFile(dir, boot.name); + var count = placedDisks.size() + placedBoots.size(); + copyEntry(content, target, size, count, totalItems); + boot.target = target; + placedBoots.add(boot); + } + } + + private void unpack() throws Exception { + if (!targetDir.exists() && !targetDir.mkdirs()) + throw new IOException(fmt("Cannot create target dir: %s", targetDir)); + try ( + var in = new FileInputStream(srcPath); + var pkg = PackageInput.open(in) + ) { + importedManifest = pkg.manifest; + totalItems = importedManifest.disks.size() + importedManifest.boots.size(); + var data = DataItem.newObject(); + data.set("done", 0); + data.set("total", totalItems); + emit(data, Phase.PACK); + var tar = new TarReader(pkg.data); + tar.forEach(this::onTarItem); + var buf = new byte[BUFFER]; + //noinspection StatementWithEmptyBody + while (pkg.data.read(buf) >= 0); + pkg.validateDataConsumed(); + } + var vm = new VMConfig(importedManifest.vm.toJson()); + vm.setId(UUID.randomUUID()); + remapDiskPaths(vm, placedDisks); + remapBootPaths(vm, placedBoots); + importedVM = vm; + } + + @NonNull + private JSONArray importNetworks( + @NonNull VMConfig vm, + @NonNull List configs + ) throws Exception { + var refs = new HashMap(); + var created = new JSONArray(); + if (networkMode.equals("skip")) { + remapNetworks(vm, refs); + return created; + } + var store = server.getContext().getNetworks(); + for (var source : configs) { + var ref = source.item.optString("pkg_network_ref", ""); + if (ref.isEmpty()) continue; + if (networkMode.equals("existing")) { + var existing = store.findByName(source.getName()); + if (existing != null) refs.put(ref, existing.getId().toString()); + continue; + } + var cfg = new NetworkConfig(source.toJson()); + cfg.item.remove("pkg_network_ref"); + cfg.setId(UUID.randomUUID()); + cfg.setName(uniqueNetworkName(cfg.getName())); + makeBridgeNameUnique(cfg); + var id = store.createNetwork(cfg); + if (id == null || id.isEmpty()) continue; + refs.put(ref, id); + created.put(cfg.toJson()); + } + remapNetworks(vm, refs); + return created; + } + + private void remapNetworks( + @NonNull VMConfig vm, + @NonNull HashMap refs + ) { + var nets = vm.item.opt("networks", null); + if (nets == null || !nets.is(DataItem.Type.ARRAY)) return; + for (var nic : nets.asArray()) { + if (!nic.is(DataItem.Type.OBJECT)) continue; + var ref = nic.optString("pkg_network_ref", ""); + nic.remove("pkg_network_ref"); + var id = refs.get(ref); + if (id == null || id.isEmpty()) nic.remove("network_id"); + else nic.set("network_id", id); + } + } + + @NonNull + private String uniqueNetworkName(@NonNull String name) { + var store = server.getContext().getNetworks(); + if (store.findByName(name) == null) return name; + int i = 1; + while (store.findByName(fmt("%s_%d", name, i)) != null) i++; + return fmt("%s_%d", name, i); + } + + private void makeBridgeNameUnique(@NonNull NetworkConfig cfg) { + var bridge = cfg.getBridgeName(); + if (bridge == null || bridge.isEmpty()) return; + if (isBridgeNameUnique(bridge)) return; + int i = 1; + while (!isBridgeNameUnique(fmt("%s%d", bridge, i))) i++; + cfg.setBridgeName(fmt("%s%d", bridge, i)); + } + + private boolean isBridgeNameUnique(@NonNull String bridgeName) { + var unique = new AtomicBoolean(true); + server.getContext().getNetworks().forEach((id, net) -> { + if (bridgeName.equals(net.getBridgeName())) unique.set(false); + }); + return unique.get(); + } + + private void copyEntry( + @NonNull InputStream in, + @NonNull File out, + long size, + int doneItems, + int totalItems + ) throws IOException { + var data = DataItem.newObject(); + data.set("done", doneItems); + data.set("total", totalItems); + data.set("file", basename(out.getPath())); + data.set("bytes_done", 0); + data.set("bytes_total", size); + emit(data, Phase.PACK); + try (var os = new FileOutputStream(out)) { + var buf = new byte[BUFFER]; + long rem = size; + long written = 0; + long lastEmit = System.currentTimeMillis(); + while (rem > 0) { + int toRead = (int) Math.min(buf.length, rem); + int n = in.read(buf, 0, toRead); + if (n <= 0) throw new IOException(fmt( + "entry %s: short read", out.getName() + )); + os.write(buf, 0, n); + rem -= n; + written += n; + var now = System.currentTimeMillis(); + if (now - lastEmit >= PROGRESS_INTERVAL_MS) { + data.set("bytes_done", written); + emit(data, Phase.PACK); + lastEmit = now; + } + } + } + data.set("done", doneItems + 1); + data.set("bytes_done", size); + emit(data, Phase.PACK); + } + + @NonNull + private String uniqueVMName(@NonNull String name) { + var store = server.getContext().getVMs(); + if (store.findByName(name) == null) return name; + int i = 1; + while (store.findByName(fmt("%s (%d)", name, i)) != null) i++; + return fmt("%s (%d)", name, i); + } + + private void emit(@NonNull DataItem item, @NonNull Phase phase) { + try { + var data = item.toJson(); + data.put("event", "vm_import_status"); + data.put("task_id", taskId.toString()); + data.put("phase", phase.name().toLowerCase()); + var ev = new JSONObject(); + ev.put("type", "event"); + ev.put("data", data); + server.broadcastEvent(ev); + } catch (Exception e) { + Log.w(TAG, fmt("Failed to emit import event for %s", taskId), e); + } + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/pkg/VMImportUtils.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/pkg/VMImportUtils.java new file mode 100644 index 000000000..4f47f12a3 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/pkg/VMImportUtils.java @@ -0,0 +1,67 @@ +package cn.classfun.droidvm.daemon.vm.pkg; + +import static java.util.Objects.requireNonNull; +import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; + +import androidx.annotation.NonNull; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; + +import cn.classfun.droidvm.lib.pkg.BootFile; +import cn.classfun.droidvm.lib.pkg.DiskEntry; +import cn.classfun.droidvm.lib.store.base.DataItem; +import cn.classfun.droidvm.lib.store.vm.BootConfig; +import cn.classfun.droidvm.lib.store.vm.VMConfig; + +public final class VMImportUtils { + private VMImportUtils() { + } + + public static void remapDiskPaths( + @NonNull VMConfig vm, + @NonNull ArrayList placed + ) { + var disks = DataItem.newArray(); + for (var disk : placed) { + if (disk.target == null || disk.ref == null) continue; + var item = DataItem.newObject(); + item.set("path", disk.target.getPath()); + item.set("readonly", disk.ref.readonly); + item.set("bus", disk.ref.bus); + disks.append(item); + } + vm.item.set("disks", disks); + } + + public static void remapBootPaths( + @NonNull VMConfig vm, + @NonNull ArrayList placed + ) { + var byKind = new HashMap(); + for (var boot : placed) + byKind.put(boot.kind, boot.target.getPath()); + var boot = BootConfig.of(vm); + if (byKind.containsKey("uefi")) + boot.setUefiFirmware(requireNonNull(byKind.get("uefi"))); + if (byKind.containsKey("kernel")) + boot.setKernel(requireNonNull(byKind.get("kernel"))); + if (byKind.containsKey("initrd")) + boot.setInitrd(requireNonNull(byKind.get("initrd"))); + } + + @NonNull + public static File uniqueFile( + @NonNull File dir, + @NonNull String name + ) { + var dot = name.lastIndexOf('.'); + String stem = dot > 0 ? name.substring(0, dot) : name; + String ext = dot > 0 ? name.substring(dot) : ""; + var f = new File(dir, name); + int i = 1; + while (f.exists()) f = new File(dir, fmt("%s_%d%s", stem, i++, ext)); + return f; + } +} From dd6a447f2c3778fa4a792e2846bc9252b1327e08 Mon Sep 17 00:00:00 2001 From: BigfootACA Date: Tue, 7 Jul 2026 15:40:28 +0800 Subject: [PATCH 114/182] ui: add VMPkgExportActivity and VMPkgImportActivity Signed-off-by: BigfootACA --- app/src/main/AndroidManifest.xml | 27 + .../ui/main/home/MainHomeFragment.java | 3 +- .../droidvm/ui/main/vm/MainVMFragment.java | 3 + .../vm/pkg/exports/VMPkgExportActivity.java | 520 +++++++++++++++++ .../pkg/exports/VMPkgExportDiskAdapter.java | 89 +++ .../ui/vm/pkg/imports/NetworkImportMode.java | 28 + .../vm/pkg/imports/VMPkgImportActivity.java | 532 ++++++++++++++++++ .../pkg/imports/VMPkgImportDiskAdapter.java | 50 ++ .../imports/VMPkgImportNetworkAdapter.java | 50 ++ .../main/res/layout/activity_vmpkg_export.xml | 219 +++++++ .../main/res/layout/activity_vmpkg_import.xml | 248 ++++++++ app/src/main/res/menu/menu_vm_actions.xml | 5 + app/src/main/res/values-zh-rCN/strings.xml | 56 ++ app/src/main/res/values-zh-rTW/strings.xml | 56 ++ app/src/main/res/values/colors.xml | 2 + app/src/main/res/values/strings.xml | 56 ++ 16 files changed, 1943 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/exports/VMPkgExportActivity.java create mode 100644 app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/exports/VMPkgExportDiskAdapter.java create mode 100644 app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/imports/NetworkImportMode.java create mode 100644 app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/imports/VMPkgImportActivity.java create mode 100644 app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/imports/VMPkgImportDiskAdapter.java create mode 100644 app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/imports/VMPkgImportNetworkAdapter.java create mode 100644 app/src/main/res/layout/activity_vmpkg_export.xml create mode 100644 app/src/main/res/layout/activity_vmpkg_import.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 65cd1970c..195153a99 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -93,6 +93,33 @@ android:theme="@style/Theme.DroidVM.NoActionBar.Dark" android:windowSoftInputMode="adjustResize" /> + + + + + + + + + + + + + + + + @@ -102,6 +103,8 @@ protected boolean onMenuClicked(@NonNull VMConfig config, @NonNull MenuItem item startActivity(intent); } else if (id == R.id.menu_vm_delete) { confirmDeleteVM(config); + } else if (id == R.id.menu_vm_export) { + startActivity(VMPkgExportActivity.createIntent(ctx, config.getId())); } return true; } diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/exports/VMPkgExportActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/exports/VMPkgExportActivity.java new file mode 100644 index 000000000..0a88f7568 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/exports/VMPkgExportActivity.java @@ -0,0 +1,520 @@ +package cn.classfun.droidvm.ui.vm.pkg.exports; + +import static android.view.View.GONE; +import static android.view.View.VISIBLE; +import static android.widget.Toast.LENGTH_LONG; +import static cn.classfun.droidvm.lib.pkg.PackageConstants.EXTENSION; +import static cn.classfun.droidvm.lib.size.SizeUtils.formatSize; +import static cn.classfun.droidvm.lib.store.enums.Enums.optEnum; +import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; +import static cn.classfun.droidvm.lib.utils.StringUtils.resolveUriPath; +import static cn.classfun.droidvm.lib.utils.ThreadUtils.runOnPool; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.activity.OnBackPressedCallback; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts.CreateDocument; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.appcompat.app.AppCompatActivity; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.appbar.CollapsingToolbarLayout; +import com.google.android.material.appbar.MaterialToolbar; +import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton; +import com.google.android.material.progressindicator.LinearProgressIndicator; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.File; +import java.util.UUID; + +import cn.classfun.droidvm.R; +import cn.classfun.droidvm.lib.archive.Compression; +import cn.classfun.droidvm.lib.daemon.DaemonConnection; +import cn.classfun.droidvm.lib.pkg.DiskRef; +import cn.classfun.droidvm.lib.pkg.Phase; +import cn.classfun.droidvm.lib.pkg.PackageConstants; +import cn.classfun.droidvm.lib.store.base.DataItem; +import cn.classfun.droidvm.lib.store.vm.VMConfig; +import cn.classfun.droidvm.lib.store.vm.VMStore; +import cn.classfun.droidvm.lib.utils.ShareUtils; +import cn.classfun.droidvm.ui.widgets.container.CollapsibleContainer; +import cn.classfun.droidvm.ui.widgets.row.ChooseRowWidget; + +public final class VMPkgExportActivity extends AppCompatActivity + implements DaemonConnection.EventListener { + public static final String EXTRA_VM_ID = "vm_id"; + private final Handler mainHandler = new Handler(Looper.getMainLooper()); + private CollapsingToolbarLayout collapsingToolbar; + private CollapsibleContainer ccDisks; + private CollapsibleContainer ccOptions; + private MaterialToolbar toolbar; + private RecyclerView containerDisks; + private TextView tvVMName; + private TextView tvVMDetail; + private TextView tvNoDisks; + private TextView tvDiskSummary; + private ChooseRowWidget chooseCompression; + private TextView tvCompressionDesc; + private LinearProgressIndicator pbRun; + private TextView tvStatus; + private TextView tvFileName; + private TextView tvProgressDetail; + private ExtendedFloatingActionButton fabCreate; + private ExtendedFloatingActionButton fabShare; + private VMConfig config; + private Compression compression = PackageConstants.DEFAULT_COMPRESSION; + private VMPkgExportDiskAdapter adapter; + private String pendingTaskId = null; + private String path = null; + private boolean exporting = false; + private Phase phase = Phase.IDLE; + private final ActivityResultLauncher createDocLauncher = + registerForActivityResult(new CreateDocument(PackageConstants.MIME), this::onDocPicked); + + @NonNull + public static Intent createIntent(@NonNull Context ctx, @NonNull UUID vmId) { + var intent = new Intent(ctx, VMPkgExportActivity.class); + intent.putExtra(EXTRA_VM_ID, vmId.toString()); + return intent; + } + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_vmpkg_export); + collapsingToolbar = findViewById(R.id.collapsing_toolbar); + ccDisks = findViewById(R.id.cc_disks); + ccOptions = findViewById(R.id.cc_options); + toolbar = findViewById(R.id.toolbar); + tvVMName = findViewById(R.id.tv_vm_name); + tvVMDetail = findViewById(R.id.tv_vm_detail); + containerDisks = findViewById(R.id.container_disks); + tvNoDisks = findViewById(R.id.tv_no_disks); + tvDiskSummary = findViewById(R.id.tv_disk_summary); + chooseCompression = findViewById(R.id.choose_compression); + tvCompressionDesc = findViewById(R.id.tv_compression_desc); + pbRun = findViewById(R.id.pb_run); + tvStatus = findViewById(R.id.tv_status); + tvFileName = findViewById(R.id.tv_filename); + tvProgressDetail = findViewById(R.id.tv_progress_detail); + fabCreate = findViewById(R.id.fab_create); + fabShare = findViewById(R.id.fab_share); + initialize(); + } + + private void initialize() { + var store = new VMStore(); + store.load(this); + var id = getIntent().getStringExtra(EXTRA_VM_ID); + if (id == null) { + finish(); + return; + } + var vmId = UUID.fromString(id); + config = store.findById(vmId); + if (config == null) { + finish(); + return; + } + collapsingToolbar.setTitle(getString(R.string.vmpkg_export_title)); + tvVMName.setText(config.getName()); + tvVMDetail.setText(getString( + R.string.vm_item_info, + config.item.optLong("cpu_count", 0), + config.item.optLong("memory_mb", 0) + )); + adapter = new VMPkgExportDiskAdapter(); + adapter.setOnSelectionChanged(this::updateDiskSummary); + containerDisks.setLayoutManager(new LinearLayoutManager(this)); + containerDisks.setAdapter(adapter); + bindDisks(); + bindCompression(); + var handler = new VMPkgExportOnBackPressedCallback(); + toolbar.setNavigationOnClickListener(v -> handler.handleOnBackPressed()); + getOnBackPressedDispatcher().addCallback(this, handler); + fabCreate.setOnClickListener(v -> startPicker()); + fabShare.setOnClickListener(v -> startShare()); + } + + private class VMPkgExportOnBackPressedCallback extends OnBackPressedCallback { + public VMPkgExportOnBackPressedCallback() { + super(true); + } + + @Override + public void handleOnBackPressed() { + if (!exporting) finish(); + } + } + + private void startShare() { + if (path == null) return; + var f = new File(path); + if (!f.exists()) return; + ShareUtils.shareFile( + this, f, PackageConstants.MIME, + getString(R.string.vmpkg_export_share_title), + msg -> showToast(R.string.vmpkg_export_failed, msg) + ); + } + + private void bindCompression() { + chooseCompression.configure( + Compression.class, + PackageConstants.DEFAULT_COMPRESSION + ); + chooseCompression.setOnValueChangedListener( + this::updateCompressionDesc + ); + updateCompressionDesc(); + } + + private void updateCompressionDesc() { + compression = chooseCompression.getSelectedItem(); + tvCompressionDesc.setText(compression.desc); + } + + @SuppressLint("NotifyDataSetChanged") + private void bindDisks() { + adapter.disks.clear(); + adapter.selected.clear(); + var arr = config.item.opt("disks", DataItem.newArray()); + for (int i = 0; i < arr.size(); i++) { + var e = arr.get(i); + if (!e.is(DataItem.Type.OBJECT)) continue; + var d = new DiskRef(i, e); + if (d.path == null || d.path.isEmpty()) continue; + adapter.disks.add(d); + if (!d.isCDROM()) adapter.selected.add(i); + } + adapter.notifyDataSetChanged(); + tvNoDisks.setVisibility(adapter.disks.isEmpty() ? VISIBLE : GONE); + updateDiskSummary(); + } + + private void updateDiskSummary() { + int selectedCount = 0; + long totalSize = 0; + for (var disk : adapter.disks) { + if (!adapter.selected.contains(disk.index)) continue; + selectedCount++; + if (disk.path != null && !disk.path.isEmpty()) + totalSize += new File(disk.path).length(); + } + tvDiskSummary.setText(getString( + R.string.vmpkg_export_disk_summary, + selectedCount, + adapter.disks.size(), + formatSize(totalSize) + )); + } + + @Override + protected void onStart() { + super.onStart(); + DaemonConnection.getInstance().addListener(this); + } + + @Override + protected void onStop() { + super.onStop(); + DaemonConnection.getInstance().removeListener(this); + } + + private void startPicker() { + if (config == null || exporting) return; + if (!validateSelectedDisks()) return; + var inst = DaemonConnection.getInstance(); + DaemonConnection.OnError onError = e -> + onExportFailure(e.getMessage()); + DaemonConnection.OnUnsuccessful onUnsuccessful = resp -> + onExportFailure(resp.optString("message", "request failed")); + DaemonConnection.OnResponse onStatus = resp -> mainHandler.post(() -> { + if (resp.optString("state").equals("stopped")) { + launchCreateDocument(); + } else showToast( + R.string.vmpkg_export_failed, + getString(R.string.vmpkg_export_vm_must_stop) + ); + }); + DaemonConnection.OnResponse onExists = resp -> { + if (!resp.optBoolean("exists", false)) { + mainHandler.post(this::launchCreateDocument); + return; + } + inst.buildRequest("vm_status") + .put("vm_id", config.getId()) + .onResponse(onStatus) + .onUnsuccessful(onUnsuccessful) + .onError(onError) + .invoke(); + }; + inst.buildRequest("vm_exists") + .put("vm_id", config.getId()) + .onResponse(onExists) + .onUnsuccessful(onUnsuccessful) + .onError(onError) + .invoke(); + } + + private void launchCreateDocument() { + var base = config.getName(); + var sb = new StringBuilder(); + for (int i = 0; i < base.length(); i++) { + char ch = base.charAt(i); + if ("\\/:*?\"<>|".indexOf(ch) >= 0 || ch < 0x20) ch = '_'; + sb.append(ch); + } + if (sb.length() == 0) sb.append("vm"); + var name = sb.toString(); + var ext = fmt(".%s", EXTENSION); + if (!name.endsWith(ext)) name += ext; + createDocLauncher.launch(name); + } + + private boolean validateSelectedDisks() { + for (var disk : adapter.disks) { + if (!adapter.selected.contains(disk.index)) continue; + if (disk.path == null || disk.path.isEmpty() || !new File(disk.path).isFile()) { + showToast(R.string.vmpkg_export_failed, getString( + R.string.vmpkg_export_disk_missing, + disk.path == null ? "" : disk.path + )); + return false; + } + } + return true; + } + + private void onDocPicked(@Nullable Uri uri) { + if (uri == null || exporting) return; + runOnPool(() -> startExport(uri)); + } + + private void startExport(@NonNull Uri uri) { + var destPath = resolveUriPath(this, uri); + if (destPath == null || destPath.isEmpty()) { + showToast(R.string.vmpkg_export_failed_path_resolve); + return; + } + this.path = destPath; + var diskIndices = new JSONArray(); + for (int i = 0; i < adapter.disks.size(); i++) { + var idx = adapter.disks.get(i).index; + if (adapter.selected.contains(idx)) diskIndices.put(idx); + } + var inst = DaemonConnection.getInstance(); + mainHandler.post(() -> { + setExporting(true); + fabCreate.setVisibility(GONE); + fabShare.setVisibility(GONE); + tvStatus.setText(R.string.vmpkg_export_running); + tvProgressDetail.setText(""); + tvFileName.setText(""); + ccDisks.setVisibility(GONE); + ccOptions.setVisibility(GONE); + }); + phase = Phase.IDLE; + DaemonConnection.OnError err = e -> onExportFailure(e.getMessage()); + DaemonConnection.OnUnsuccessful f = resp -> + onExportFailure(resp.optString("message", "request failed")); + DaemonConnection.OnResponse onExport = resp -> { + var tid = resp.optString("task_id"); + if (!tid.isEmpty()) pendingTaskId = tid; + else onExportFailure(getString(R.string.vmpkg_export_failed_no_task)); + }; + DaemonConnection.OnResponse onCreateModify = resp -> + inst.buildRequest("vm_export") + .put("vm_id", config.getId()) + .put("disks", diskIndices) + .put("dest_path", destPath) + .put("compression", compression) + .onResponse(onExport) + .onUnsuccessful(f) + .onError(err) + .invoke(); + DaemonConnection.OnResponse onExists = resp -> + inst.buildRequest(resp.optBoolean("exists", false) ? "vm_modify" : "vm_create") + .put("config", config) + .onResponse(onCreateModify) + .onUnsuccessful(f) + .onError(err) + .invoke(); + inst.buildRequest("vm_exists") + .put("vm_id", config.getId()) + .onResponse(onExists) + .onUnsuccessful(f) + .onError(err) + .invoke(); + } + + @Override + public void onDaemonEvent(JSONObject msg) { + switch (phase) { + case DONE: + case ERROR: + return; + } + if (msg == null) return; + if (!msg.optString("type").equals("event")) return; + var data = msg.optJSONObject("data"); + if (data == null) return; + if (!data.optString("event").equals("vm_export_status")) return; + var tid = data.optString("task_id"); + if (pendingTaskId == null || !pendingTaskId.equals(tid)) return; + var phase = optEnum(data, "phase", Phase.SCAN); + var done = data.optInt("done"); + var total = data.optInt("total"); + var bytesDone = data.optLong("bytes_done"); + var bytesTotal = data.optLong("bytes_total"); + var file = data.optString("file"); + var message = data.optString("message"); + mainHandler.post(() -> onProgress( + phase, done, total, bytesDone, bytesTotal, file, message + )); + } + + @Override + public void onDaemonConnected() { + } + + @Override + public void onDaemonDisconnected() { + switch (phase) { + case IDLE: + case DONE: + case ERROR: + return; + } + onExportFailure(getString( + R.string.vmpkg_export_failed, + "daemon dead" + )); + pendingTaskId = null; + } + + private void onProgress( + @NonNull Phase phase, + int done, + int total, + long bytesDone, + long bytesTotal, + @NonNull String file, + @NonNull String message + ) { + this.phase = phase; + switch (phase) { + case SCAN: + tvStatus.setText(R.string.vmpkg_export_scan); + applyProgress(done, total, bytesDone, bytesTotal); + break; + case PACK: + tvStatus.setText(R.string.vmpkg_export_pack); + applyProgress(done, total, bytesDone, bytesTotal); + applyProgressDetail(file, bytesDone, bytesTotal); + break; + case DONE: + pbRun.setVisibility(GONE); + tvProgressDetail.setText(""); + tvFileName.setText(""); + fabShare.setVisibility(VISIBLE); + tvStatus.setText(getString( + R.string.vmpkg_export_success_path, path + )); + showToast(R.string.vmpkg_export_success); + pendingTaskId = null; + setExporting(false); + setResult(Activity.RESULT_OK); + break; + case ERROR: + onExportFailure(message); + pendingTaskId = null; + break; + default: + break; + } + } + + private void onExportFailure(@Nullable String msg) { + if (msg == null) msg = getString(R.string.vmpkg_export_failed_no_task); + var finalMsg = msg; + mainHandler.post(() -> { + pbRun.setVisibility(GONE); + setExporting(false); + tvProgressDetail.setText(""); + tvFileName.setText(""); + fabCreate.setVisibility(VISIBLE); + tvStatus.setText(getString(R.string.vmpkg_export_failed, finalMsg)); + ccDisks.setVisibility(VISIBLE); + ccOptions.setVisibility(VISIBLE); + showToast(R.string.vmpkg_export_failed, finalMsg); + }); + } + + private void setExporting(boolean exporting) { + this.exporting = exporting; + pbRun.setVisibility(exporting ? VISIBLE : GONE); + pbRun.setIndeterminate(exporting); + chooseCompression.setEnabled(!exporting); + containerDisks.setEnabled(!exporting); + adapter.setEnabled(!exporting); + } + + private void applyProgressDetail( + @NonNull String file, + long bytesDone, + long bytesTotal + ) { + if (file.isEmpty() || bytesTotal <= 0 || bytesDone < 0) { + tvProgressDetail.setText(""); + return; + } + var done = Math.min(bytesDone, bytesTotal); + tvProgressDetail.setText(getString( + R.string.vmpkg_export_progress_detail, + formatSize(done), + formatSize(bytesTotal), + done * 100 / bytesTotal + )); + tvFileName.setText(file); + } + + private void showToast(@StringRes int id, Object... args) { + mainHandler.post(() -> Toast.makeText( + this, getString(id, args), LENGTH_LONG + ).show()); + } + + private void applyProgress(int done, int total, long bytesDone, long bytesTotal) { + if (total <= 0) return; + pbRun.setIndeterminate(false); + if (bytesTotal > 0 && bytesDone >= 0) { + int unit = 1000; + long clampedBytes = Math.min(bytesDone, bytesTotal); + int scaled = (int) ((clampedBytes * unit) / bytesTotal); + pbRun.setMax(total * unit); + pbRun.setProgress(Math.min( + done * unit + scaled, + total * unit + )); + } else { + pbRun.setMax(total); + pbRun.setProgress(done); + } + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/exports/VMPkgExportDiskAdapter.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/exports/VMPkgExportDiskAdapter.java new file mode 100644 index 000000000..11d6522ce --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/exports/VMPkgExportDiskAdapter.java @@ -0,0 +1,89 @@ +package cn.classfun.droidvm.ui.vm.pkg.exports; + +import static android.view.View.GONE; +import static android.view.View.VISIBLE; +import static cn.classfun.droidvm.lib.utils.StringUtils.basename; +import static cn.classfun.droidvm.lib.utils.StringUtils.dirname; + +import android.annotation.SuppressLint; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.appcompat.content.res.AppCompatResources; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.color.MaterialColors; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import cn.classfun.droidvm.R; +import cn.classfun.droidvm.lib.pkg.DiskRef; +import cn.classfun.droidvm.ui.main.base.BaseViewHolder; + +public final class VMPkgExportDiskAdapter extends RecyclerView.Adapter { + public final List disks = new ArrayList<>(); + public final Set selected = new HashSet<>(); + private boolean enabled = true; + private Runnable onSelectionChanged; + + public void setOnSelectionChanged(@NonNull Runnable onSelectionChanged) { + this.onSelectionChanged = onSelectionChanged; + } + + @SuppressLint("NotifyDataSetChanged") + public void setEnabled(boolean enabled) { + if (this.enabled == enabled) return; + this.enabled = enabled; + notifyDataSetChanged(); + } + + @NonNull + @Override + public BaseViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + var inf = LayoutInflater.from(parent.getContext()); + var v = inf.inflate(R.layout.item_main_list, parent, false); + return new BaseViewHolder(v); + } + + @Override + public void onBindViewHolder(@NonNull BaseViewHolder h, int position) { + var disk = disks.get(position); + var ctx = h.itemView.getContext(); + var isSel = selected.contains(disk.index); + h.itemName.setText(basename(disk.path)); + h.itemInfo.setText(dirname(disk.path)); + h.itemInfo.setVisibility(VISIBLE); + h.itemState.setVisibility(GONE); + h.itemAction.setVisibility(GONE); + h.itemIcon.setVisibility(VISIBLE); + var icon = disk.isCDROM() ? R.drawable.ic_cdrom : R.drawable.ic_nav_disk; + h.itemIcon.setImageDrawable(AppCompatResources.getDrawable(ctx, icon)); + var bg = MaterialColors.getColor(h.itemCard, isSel + ? com.google.android.material.R.attr.colorSurfaceVariant + : com.google.android.material.R.attr.colorSurface); + h.itemCard.setCardBackgroundColor(bg); + h.itemCard.setEnabled(enabled); + h.itemCard.setAlpha(enabled ? 1f : 0.64f); + h.itemCard.setOnClickListener(enabled ? v -> toggleSelection(disk.index) : null); + } + + @SuppressLint("NotifyDataSetChanged") + private void toggleSelection(int index) { + if (selected.contains(index)) { + selected.remove(index); + } else { + selected.add(index); + } + if (onSelectionChanged != null) onSelectionChanged.run(); + notifyDataSetChanged(); + } + + @Override + public int getItemCount() { + return disks.size(); + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/imports/NetworkImportMode.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/imports/NetworkImportMode.java new file mode 100644 index 000000000..4b89116ef --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/imports/NetworkImportMode.java @@ -0,0 +1,28 @@ +package cn.classfun.droidvm.ui.vm.pkg.imports; + +import androidx.annotation.NonNull; +import androidx.annotation.StringRes; + +import cn.classfun.droidvm.R; +import cn.classfun.droidvm.lib.store.enums.StringEnum; + +public enum NetworkImportMode implements StringEnum { + SKIP(R.string.vmpkg_import_network_skip, "skip"), + EXISTING(R.string.vmpkg_import_network_existing, "existing"), + AUTO(R.string.vmpkg_import_network_auto, "auto"); + + @StringRes + private final int stringId; + @NonNull + final String id; + + NetworkImportMode(@StringRes int stringId, @NonNull String id) { + this.stringId = stringId; + this.id = id; + } + + @Override + public int getStringId() { + return stringId; + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/imports/VMPkgImportActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/imports/VMPkgImportActivity.java new file mode 100644 index 000000000..e3283c07b --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/imports/VMPkgImportActivity.java @@ -0,0 +1,532 @@ +package cn.classfun.droidvm.ui.vm.pkg.imports; + +import static android.view.View.GONE; +import static android.view.View.VISIBLE; +import static android.widget.Toast.LENGTH_LONG; +import static android.widget.Toast.LENGTH_SHORT; +import static cn.classfun.droidvm.lib.size.SizeUtils.formatSize; +import static cn.classfun.droidvm.lib.store.enums.Enums.optEnum; +import static cn.classfun.droidvm.lib.utils.FileUtils.externalPath; +import static cn.classfun.droidvm.lib.utils.StringUtils.pathJoin; +import static cn.classfun.droidvm.lib.utils.StringUtils.resolveUriPath; +import static cn.classfun.droidvm.lib.utils.ThreadUtils.runOnPool; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.storage.StorageManager; +import android.view.View; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts.OpenDocument; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.appbar.CollapsingToolbarLayout; +import com.google.android.material.appbar.MaterialToolbar; +import com.google.android.material.button.MaterialButton; +import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton; +import com.google.android.material.progressindicator.LinearProgressIndicator; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.File; +import java.io.IOException; +import java.text.DateFormat; +import java.util.Date; + +import cn.classfun.droidvm.R; +import cn.classfun.droidvm.lib.daemon.DaemonConnection; +import cn.classfun.droidvm.lib.pkg.PackageConstants; +import cn.classfun.droidvm.lib.pkg.PackageInput; +import cn.classfun.droidvm.lib.pkg.PackageManifest; +import cn.classfun.droidvm.lib.pkg.Phase; +import cn.classfun.droidvm.lib.store.disk.DiskConfig; +import cn.classfun.droidvm.lib.store.disk.DiskStore; +import cn.classfun.droidvm.lib.store.network.NetworkConfig; +import cn.classfun.droidvm.lib.store.network.NetworkStore; +import cn.classfun.droidvm.lib.store.vm.VMConfig; +import cn.classfun.droidvm.lib.store.vm.VMStore; +import cn.classfun.droidvm.ui.vm.info.VMInfoActivity; +import cn.classfun.droidvm.ui.widgets.container.CollapsibleContainer; +import cn.classfun.droidvm.ui.widgets.row.ChooseRowWidget; +import cn.classfun.droidvm.ui.widgets.row.TextInputRowWidget; + +public final class VMPkgImportActivity extends AppCompatActivity + implements DaemonConnection.EventListener { + private final Handler mainHandler = new Handler(Looper.getMainLooper()); + private final String defaultPath = pathJoin(externalPath(), "DroidVM"); + private CollapsingToolbarLayout collapsingToolbar; + private MaterialToolbar toolbar; + private MaterialButton btnPick; + private ExtendedFloatingActionButton btnImport; + private LinearProgressIndicator pbRun; + private TextView tvError; + private TextView tvVmName; + private TextView tvVmDetail; + private TextView tvPackageMeta; + private TextView tvDiskSummary; + private TextInputRowWidget inputTarget; + private CollapsibleContainer ccNetworks; + private RecyclerView containerDisks; + private RecyclerView containerNetworks; + private ChooseRowWidget chooseNetworkMode; + private TextView tvStatus; + private TextView tvFile; + private TextView tvProgressDetail; + private View groupSummary; + private Uri pickedUri; + private PackageManifest preview; + private VMPkgImportDiskAdapter diskAdapter; + private VMPkgImportNetworkAdapter networkAdapter; + private String pendingTaskId = null; + private boolean importing = false; + private NetworkImportMode networkMode = NetworkImportMode.AUTO; + private final ActivityResultLauncher openDocLauncher = + registerForActivityResult(new OpenDocument(), this::onDocPicked); + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_vmpkg_import); + collapsingToolbar = findViewById(R.id.collapsing_toolbar); + toolbar = findViewById(R.id.toolbar); + btnPick = findViewById(R.id.btn_pick); + btnImport = findViewById(R.id.btn_import); + pbRun = findViewById(R.id.pb_run); + tvError = findViewById(R.id.tv_error); + tvVmName = findViewById(R.id.tv_vm_name); + tvVmDetail = findViewById(R.id.tv_vm_detail); + tvPackageMeta = findViewById(R.id.tv_package_meta); + tvDiskSummary = findViewById(R.id.tv_disk_summary); + inputTarget = findViewById(R.id.input_target); + ccNetworks = findViewById(R.id.cc_networks); + containerDisks = findViewById(R.id.container_disks); + containerNetworks = findViewById(R.id.container_networks); + chooseNetworkMode = findViewById(R.id.choose_network_mode); + tvStatus = findViewById(R.id.tv_status); + tvFile = findViewById(R.id.tv_file); + tvProgressDetail = findViewById(R.id.tv_progress_detail); + groupSummary = findViewById(R.id.group_summary); + initialize(); + } + + private void initialize() { + collapsingToolbar.setTitle(getString(R.string.vmpkg_import_title)); + toolbar.setNavigationOnClickListener(v -> { + if (!importing) finish(); + }); + btnImport.setOnClickListener(v -> doImport()); + diskAdapter = new VMPkgImportDiskAdapter(); + networkAdapter = new VMPkgImportNetworkAdapter(); + containerDisks.setLayoutManager(new LinearLayoutManager(this)); + containerDisks.setAdapter(diskAdapter); + containerNetworks.setLayoutManager(new LinearLayoutManager(this)); + containerNetworks.setAdapter(networkAdapter); + inputTarget.setText(defaultPath); + chooseNetworkMode.configure(NetworkImportMode.class, NetworkImportMode.AUTO); + chooseNetworkMode.setOnValueChangedListener(() -> + networkMode = chooseNetworkMode.getSelectedItem()); + var filter = new String[]{PackageConstants.MIME, "*/*"}; + btnPick.setOnClickListener(v -> { + if (!importing) openDocLauncher.launch(filter); + }); + var intentUri = resolveIntentUri(getIntent()); + if (intentUri != null) { + onDocPicked(intentUri); + } else { + mainHandler.postDelayed(() -> openDocLauncher.launch(filter), 50); + } + } + + @Override + protected void onNewIntent(@NonNull Intent intent) { + super.onNewIntent(intent); + setIntent(intent); + var uri = resolveIntentUri(intent); + if (uri != null) onDocPicked(uri); + } + + @Nullable + private Uri resolveIntentUri(@Nullable Intent intent) { + if (intent == null) return null; + var data = intent.getData(); + if (data != null) return data; + var stream = intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri.class); + if (stream != null) return stream; + var clip = intent.getClipData(); + if (clip == null || clip.getItemCount() <= 0) return null; + for (int i = 0; i < clip.getItemCount(); i++) { + var uri = clip.getItemAt(i).getUri(); + if (uri != null) return uri; + } + return null; + } + + @Override + protected void onStart() { + super.onStart(); + DaemonConnection.getInstance().addListener(this); + } + + @Override + protected void onStop() { + super.onStop(); + DaemonConnection.getInstance().removeListener(this); + } + + private void onDocPicked(@Nullable Uri uri) { + if (uri == null || importing) return; + pickedUri = uri; + tvError.setVisibility(GONE); + groupSummary.setVisibility(GONE); + btnImport.setEnabled(false); + tvStatus.setText(R.string.vmpkg_import_reading); + runOnPool(() -> { + try (var is = getContentResolver().openInputStream(uri)) { + if (is == null) throw new RuntimeException("Cannot open file"); + try (var input = PackageInput.open(is)) { + preview = input.manifest; + } + mainHandler.post(this::showPreview); + } catch (Exception e) { + mainHandler.post(() -> showError(getString(R.string.vmpkg_import_invalid, e.getMessage()))); + } + }); + } + + @SuppressLint("NotifyDataSetChanged") + private void showPreview() { + if (preview == null) return; + tvVmName.setText(preview.vm.getName()); + tvVmDetail.setText(getString( + R.string.vm_item_info, + preview.vm.item.optLong("cpu_count", 0), + preview.vm.item.optLong("memory_mb", 0) + )); + var df = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT); + var appVer = preview.appVersion.isEmpty() ? "DroidVM" : preview.appVersion; + var dateStr = preview.createdAt > 0 ? df.format(new Date(preview.createdAt)) : "-"; + var meta = getString( + R.string.vmpkg_import_meta, + appVer, dateStr, + preview.manifestVersion + ); + tvPackageMeta.setText(meta.trim()); + long totalSize = 0; + diskAdapter.disks.clear(); + for (var d : preview.disks) { + totalSize += d.size; + diskAdapter.disks.add(d); + } + diskAdapter.notifyDataSetChanged(); + networkAdapter.networks.clear(); + networkAdapter.networks.addAll(preview.networks); + networkAdapter.notifyDataSetChanged(); + var hasNetworks = !preview.networks.isEmpty(); + ccNetworks.setVisibility(hasNetworks ? VISIBLE : GONE); + chooseNetworkMode.setVisibility(hasNetworks ? VISIBLE : GONE); + if (inputTarget.getText().trim().isEmpty()) + inputTarget.setText(defaultPath); + tvDiskSummary.setText(getString( + R.string.vmpkg_import_disk_summary, + preview.disks.size(), + formatSize(totalSize) + )); + tvStatus.setText(""); + btnPick.setVisibility(GONE); + groupSummary.setVisibility(VISIBLE); + btnImport.setVisibility(VISIBLE); + btnImport.setEnabled(true); + } + + private void doImport() { + if (pickedUri == null || preview == null || importing) return; + var srcPath = resolveUriPath(this, pickedUri); + if (srcPath == null || srcPath.isEmpty()) { + showError(getString( + R.string.vmpkg_import_failed, + getString(R.string.vmpkg_export_failed_path_resolve) + )); + return; + } + try { + ensureTargetSpace(importRequiredBytes(), targetFolder()); + } catch (IOException e) { + showError(getString(R.string.vmpkg_import_failed, e.getMessage())); + return; + } + setImporting(true); + tvError.setVisibility(GONE); + tvStatus.setText(R.string.vmpkg_import_running); + tvFile.setText(""); + tvProgressDetail.setText(""); + var conn = DaemonConnection.getInstance(); + conn.buildRequest("vm_import") + .put("src_path", srcPath) + .put("target_dir", targetFolder().getPath()) + .put("network_mode", networkMode.id) + .onResponse(resp -> { + var tid = resp.optString("task_id", ""); + if (!tid.isEmpty()) pendingTaskId = tid; + else onImportFailure(getString(R.string.vmpkg_export_failed_no_task)); + }) + .onUnsuccessful(resp -> onImportFailure(resp.optString("message", "request failed"))) + .onError(e -> onImportFailure(e.getMessage())) + .invoke(); + } + + @Override + public void onDaemonEvent(JSONObject msg) { + if (msg == null || !msg.optString("type", "").equals("event")) return; + var data = msg.optJSONObject("data"); + if (data == null || !data.optString("event", "").equals("vm_import_status")) return; + var tid = data.optString("task_id", ""); + if (pendingTaskId == null || !pendingTaskId.equals(tid)) return; + var phase = optEnum(data, "phase", Phase.SCAN); + var done = data.optInt("done", 0); + var total = data.optInt("total", 0); + var file = data.optString("file", ""); + var bytesDone = data.optLong("bytes_done", -1); + var bytesTotal = data.optLong("bytes_total", -1); + var message = data.optString("message", ""); + var vmId = data.optString("vm_id", ""); + var disks = data.optJSONArray("disks"); + var networks = data.optJSONArray("networks"); + mainHandler.post(() -> onProgress( + phase, done, total, file, + bytesDone, bytesTotal, + message, vmId, disks, networks + )); + } + + @Override + public void onDaemonConnected() { + } + + @Override + public void onDaemonDisconnected() { + } + + private void onProgress( + @NonNull Phase phase, + int done, + int total, + @NonNull String file, + long bytesDone, + long bytesTotal, + @NonNull String message, + @NonNull String vmId, + @Nullable JSONArray disks, + @Nullable JSONArray networks + ) { + switch (phase) { + case PACK: + tvStatus.setText(R.string.vmpkg_import_running); + applyProgress(done, total, bytesDone, bytesTotal); + applyProgressDetail(file, bytesDone, bytesTotal); + break; + case DONE: + if (vmId.isEmpty()) { + onImportFailure("missing vm_id"); + return; + } + finishImport( + vmId, + disks == null ? new JSONArray() : disks, + networks == null ? new JSONArray() : networks + ); + break; + case ERROR: + onImportFailure(message); + break; + default: + break; + } + } + + private void finishImport( + @NonNull String vmId, + @NonNull JSONArray disks, + @NonNull JSONArray networks + ) { + DaemonConnection.getInstance().buildRequest("vm_get") + .put("vm_id", vmId) + .onResponse(resp -> persistImport(resp.optJSONObject("data"), disks, networks)) + .onUnsuccessful(resp -> onImportFailure(resp.optString("message", "request failed"))) + .onError(e -> onImportFailure(e.getMessage())) + .invoke(); + } + + private void persistImport( + @Nullable JSONObject vmJson, + @NonNull JSONArray disks, + @NonNull JSONArray networks + ) { + try { + if (vmJson == null) throw new IllegalArgumentException("missing VM config"); + var vm = new VMConfig(vmJson); + var vmStore = new VMStore(); + vmStore.load(this); + if (vmStore.findById(vm.getId()) == null) vmStore.add(vm); + else vmStore.update(vm); + var diskStore = new DiskStore(); + diskStore.load(this); + for (int i = 0; i < disks.length(); i++) { + var diskJson = disks.optJSONObject(i); + if (diskJson == null) continue; + var path = diskJson.optString("path"); + var file = new File(path); + var parent = file.getParent(); + var name = file.getName(); + if (path.isEmpty()) { + name = diskJson.optString("name", ""); + parent = diskJson.optString("folder", ""); + path = pathJoin(parent, name); + } + if (!path.isEmpty() && diskStore.findByPath(path) != null) continue; + var disk = new DiskConfig(); + disk.setName(name); + disk.item.set("folder", parent == null ? "" : parent); + diskStore.add(disk); + } + var networkStore = new NetworkStore(); + networkStore.load(this); + for (int i = 0; i < networks.length(); i++) { + var netJson = networks.optJSONObject(i); + if (netJson == null) continue; + var net = new NetworkConfig(netJson); + if (networkStore.findById(net.getId()) == null) networkStore.add(net); + } + vmStore.save(this); + diskStore.save(this); + networkStore.save(this); + mainHandler.post(() -> { + setImporting(false); + pendingTaskId = null; + tvStatus.setText(getString(R.string.vmpkg_import_success, vm.getName())); + Toast.makeText(this, R.string.vmpkg_import_done, LENGTH_SHORT).show(); + setResult(Activity.RESULT_OK); + var intent = new Intent(this, VMInfoActivity.class); + intent.putExtra("target_id", vm.getId().toString()); + startActivity(intent); + finish(); + }); + } catch (Exception e) { + onImportFailure(e.getMessage()); + } + } + + private void onImportFailure(@Nullable String msg) { + var message = msg == null ? "Unknown error" : msg; + mainHandler.post(() -> { + setImporting(false); + pendingTaskId = null; + tvStatus.setText(getString(R.string.vmpkg_import_failed, message)); + showError(getString(R.string.vmpkg_import_failed, message)); + }); + } + + private void showError(@NonNull String message) { + tvError.setText(message); + tvError.setVisibility(VISIBLE); + Toast.makeText(this, message, LENGTH_LONG).show(); + } + + private void setImporting(boolean importing) { + this.importing = importing; + btnPick.setEnabled(!importing); + btnImport.setEnabled(!importing && preview != null); + chooseNetworkMode.setEnabled(!importing); + pbRun.setVisibility(importing ? VISIBLE : GONE); + pbRun.setIndeterminate(importing); + } + + private void applyProgress( + int done, + int total, + long bytesDone, + long bytesTotal + ) { + if (total <= 0) return; + pbRun.setIndeterminate(false); + if (bytesTotal > 0 && bytesDone >= 0) { + int unit = 1000; + long clampedBytes = Math.min(bytesDone, bytesTotal); + int scaled = (int) ((clampedBytes * unit) / bytesTotal); + pbRun.setMax(total * unit); + pbRun.setProgress(Math.min(done * unit + scaled, total * unit)); + } else { + pbRun.setMax(total); + pbRun.setProgress(done); + } + } + + private void applyProgressDetail( + @NonNull String file, + long bytesDone, + long bytesTotal + ) { + tvFile.setText(file); + if (bytesTotal <= 0 || bytesDone < 0) { + tvProgressDetail.setText(""); + return; + } + var done = Math.min(bytesDone, bytesTotal); + tvProgressDetail.setText(getString( + R.string.vmpkg_export_progress_detail, + formatSize(done), + formatSize(bytesTotal), + done * 100 / bytesTotal + )); + } + + @NonNull + private File targetFolder() { + var path = inputTarget.getText().trim(); + if (path.isEmpty()) path = defaultPath; + return new File(path); + } + + private long importRequiredBytes() { + long size = 0; + if (preview != null) { + for (var disk : preview.disks) size += disk.size; + for (var boot : preview.boots) size += boot.size; + } + return size; + } + + private void ensureTargetSpace(long required, @NonNull File target) throws IOException { + if (required <= 0) return; + var parent = target.getParentFile(); + var spacePath = target.exists() || parent == null ? target : parent; + var usable = spacePath.getUsableSpace(); + if (usable >= required) return; + var storage = getSystemService(StorageManager.class); + if (storage != null) { + var uuid = storage.getUuidForPath(spacePath); + var allocatable = storage.getAllocatableBytes(uuid); + if (allocatable >= required) { + storage.allocateBytes(uuid, required); + if (spacePath.getUsableSpace() >= required) return; + } + usable = Math.max(usable, allocatable); + } + throw new IOException(getString( + R.string.vmpkg_import_no_space, + formatSize(required), + formatSize(usable) + )); + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/imports/VMPkgImportDiskAdapter.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/imports/VMPkgImportDiskAdapter.java new file mode 100644 index 000000000..a7b67fab7 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/imports/VMPkgImportDiskAdapter.java @@ -0,0 +1,50 @@ +package cn.classfun.droidvm.ui.vm.pkg.imports; + +import static android.view.View.GONE; +import static android.view.View.VISIBLE; +import static cn.classfun.droidvm.lib.size.SizeUtils.formatSize; + +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.appcompat.content.res.AppCompatResources; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.List; + +import cn.classfun.droidvm.R; +import cn.classfun.droidvm.lib.pkg.DiskEntry; +import cn.classfun.droidvm.ui.main.base.BaseViewHolder; + +public final class VMPkgImportDiskAdapter extends RecyclerView.Adapter { + public final List disks = new ArrayList<>(); + + @NonNull + @Override + public BaseViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + var inf = LayoutInflater.from(parent.getContext()); + return new BaseViewHolder(inf.inflate(R.layout.item_main_list, parent, false)); + } + + @Override + public void onBindViewHolder(@NonNull BaseViewHolder h, int position) { + var disk = disks.get(position); + var ctx = h.itemView.getContext(); + h.itemName.setText(disk.name); + h.itemInfo.setText(formatSize(disk.size)); + h.itemInfo.setVisibility(VISIBLE); + h.itemState.setVisibility(GONE); + h.itemAction.setVisibility(GONE); + h.itemIcon.setVisibility(VISIBLE); + h.itemIcon.setImageDrawable(AppCompatResources.getDrawable(ctx, R.drawable.ic_nav_disk)); + h.itemCard.setEnabled(false); + h.itemCard.setClickable(false); + } + + @Override + public int getItemCount() { + return disks.size(); + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/imports/VMPkgImportNetworkAdapter.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/imports/VMPkgImportNetworkAdapter.java new file mode 100644 index 000000000..fc9a8d293 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/imports/VMPkgImportNetworkAdapter.java @@ -0,0 +1,50 @@ +package cn.classfun.droidvm.ui.vm.pkg.imports; + +import static android.view.View.GONE; +import static android.view.View.VISIBLE; + +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.appcompat.content.res.AppCompatResources; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.List; + +import cn.classfun.droidvm.R; +import cn.classfun.droidvm.lib.store.network.NetworkConfig; +import cn.classfun.droidvm.ui.main.base.BaseViewHolder; + +public final class VMPkgImportNetworkAdapter extends RecyclerView.Adapter { + public final List networks = new ArrayList<>(); + + @NonNull + @Override + public BaseViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + var inf = LayoutInflater.from(parent.getContext()); + return new BaseViewHolder(inf.inflate(R.layout.item_main_list, parent, false)); + } + + @Override + public void onBindViewHolder(@NonNull BaseViewHolder h, int position) { + var network = networks.get(position); + var ctx = h.itemView.getContext(); + var bridge = network.getBridgeName(); + h.itemName.setText(network.getName()); + h.itemInfo.setText(bridge == null || bridge.isEmpty() ? network.getUplinkMode().key() : bridge); + h.itemInfo.setVisibility(VISIBLE); + h.itemState.setVisibility(GONE); + h.itemAction.setVisibility(GONE); + h.itemIcon.setVisibility(VISIBLE); + h.itemIcon.setImageDrawable(AppCompatResources.getDrawable(ctx, R.drawable.ic_nav_network)); + h.itemCard.setEnabled(false); + h.itemCard.setClickable(false); + } + + @Override + public int getItemCount() { + return networks.size(); + } +} diff --git a/app/src/main/res/layout/activity_vmpkg_export.xml b/app/src/main/res/layout/activity_vmpkg_export.xml new file mode 100644 index 000000000..f7a850bdb --- /dev/null +++ b/app/src/main/res/layout/activity_vmpkg_export.xml @@ -0,0 +1,219 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_vmpkg_import.xml b/app/src/main/res/layout/activity_vmpkg_import.xml new file mode 100644 index 000000000..a6779ba3f --- /dev/null +++ b/app/src/main/res/layout/activity_vmpkg_import.xml @@ -0,0 +1,248 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/menu/menu_vm_actions.xml b/app/src/main/res/menu/menu_vm_actions.xml index 3c8836317..e4e6826f9 100644 --- a/app/src/main/res/menu/menu_vm_actions.xml +++ b/app/src/main/res/menu/menu_vm_actions.xml @@ -6,6 +6,11 @@ android:icon="@drawable/ic_edit" android:title="@string/vm_edit" /> + + 保存 已使用: %1$d (%2$s) 显示占用 + + 导出为包 + 导出虚拟机 + 要包含的磁盘 + 此虚拟机没有挂载磁盘。 + 已选 %1$d/%2$d,总计 %3$s + 创建包 + 其它选项 + 压缩方式 + 无 + Gzip (Deflate) + XZ (LZMA2) + Zstandard (ZSTD) + Gzip 压缩速度快、CPU 占用低,但生成的包体积较大。 + XZ (LZMA2) 压缩速度最慢、CPU 占用高,但生成的包体积最小。 + Zstandard 综合表现最佳:压缩快且压缩比良好。 + 正在提交至守护进程\u2026 + 正在扫描磁盘\u2026 + 正在打包磁盘\u2026 + %1$s / %2$s %3$d%% + 包已创建 + 包已创建:%1$s + 导出失败:%1$s + 无法解析目标路径 + 守护进程未返回任务 ID + 分享 + 分享虚拟机包 + 包名称 + 包版本 + 注释 + 导出前必须先停止虚拟机 + 选中的磁盘不存在:%1$s + 导入虚拟机包 + 选择 .vmpkg 文件 + 导入 + 正在读取包\u2026 + 正在导入\u2026 + 磁盘 + 网络 + 此包中没有磁盘。 + %1$s | %2$s | manifest v%3$d + %1$s(%2$s) + %1$d 个磁盘,%2$s + 网络:%1$s + 此包中没有绑定网络。 + 网络导入 + 跳过网络 + 按名称使用现有网络 + 自动创建网络 + 目标空间不足。需要 %1$s,可用 %2$s。 + 导入目录 + 磁盘将导入到:\n%1$s + 已导入虚拟机:%1$s + 导入完成 + 导入失败:%1$s + 不是有效的 vmpkg:%1$s diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index cc5b40e81..1f2e25709 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -1032,4 +1032,60 @@ 您的裝置上的 Gunyah 版本過舊(如驍龍 8 Gen 2),請嘗試更新系統或更換較新的裝置。 沙箱模式不支援 crosvm的沙箱模式在此平台不可用,請停用沙箱模式。 + + 匯出為包 + 匯出虛擬機器 + 要包含的磁碟 + 此虛擬機器沒有掛載磁碟。 + 已選 %1$d/%2$d,總計 %3$s + 建立包 + 其它選項 + 壓縮方式 + 無 + Gzip (Deflate) + XZ (LZMA2) + Zstandard (ZSTD) + Gzip 壓縮速度快、CPU 佔用低,但產生的包體積較大。 + XZ (LZMA2) 壓縮速度最慢、CPU 佔用高,但產生的包體積最小。 + Zstandard 綜合表現最佳:壓縮快且壓縮比良好。 + 正在提交至守護行程\u2026 + 正在掃描磁碟\u2026 + 正在打包磁碟\u2026 + %1$s / %2$s %3$d%% + 包已建立 + 包已建立:%1$s + 匯出失敗:%1$s + 無法解析目標路徑 + 守護行程未傳回工作 ID + 分享 + 分享虛擬機器包 + 包名稱 + 包版本 + 註釋 + 匯出前必須先停止虛擬機器 + 選中的磁碟不存在:%1$s + 匯入虛擬機器包 + 選擇 .vmpkg 檔案 + 匯入 + 正在讀取包\u2026 + 正在匯入\u2026 + 磁碟 + 網路 + 此包中沒有磁碟。 + %1$s | %2$s | manifest v%3$d + %1$s(%2$s) + %1$d 個磁碟,%2$s + 網路:%1$s + 此包中沒有綁定網路。 + 網路匯入 + 跳過網路 + 按名稱使用現有網路 + 自動建立網路 + 目標空間不足。需要 %1$s,可用 %2$s。 + 匯入目錄 + 磁碟將匯入到:\n%1$s + 已匯入虛擬機器:%1$s + 匯入完成 + 匯入失敗:%1$s + 不是有效的 vmpkg:%1$s diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 665b2beb4..e22a01568 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -29,4 +29,6 @@ #FF4CAF50 #FFF44336 + + #FF888888 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8a2f24f09..0f264f433 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1069,4 +1069,60 @@ The Gunyah version on your device is too old (for example, Snapdragon 8 Gen 2). Try updating the system or switching to a newer device. Sandbox is not supported crosvm\'s sandbox is not available on this platform. Please disable sandbox. + + Export as package + Export VM + Disks to include + This VM has no attached disks. + Selected %1$d/%2$d, total %3$s + Create package + Other options + Compression + None + Gzip (Deflate) + XZ (LZMA2) + Zstandard (ZSTD) + Gzip is fast with low CPU usage but produces larger archives. + XZ (LZMA2) is slowest and CPU-heavy but produces the smallest archives. + Zstandard offers the best balance: fast compression with good ratios. + Submitting to daemon\u2026 + Scanning disks\u2026 + Packaging disks\u2026 + %1$s / %2$s %3$d%% + Package created + Package created: %1$s + Export failed: %1$s + Cannot resolve destination path + Daemon did not return a task id + Share + Share VM package + Package name + Package version + Comment + The VM must be stopped before export + Selected disk does not exist: %1$s + Import VM package + Pick .vmpkg file + Import + Reading package\u2026 + Importing\u2026 + Disks + Networks + No disks in this package. + %1$s | %2$s | manifest v%3$d + %1$s (%2$s) + %1$d disks, %2$s + Network: %1$s + No bound networks in this package. + Network import + Skip networks + Use existing networks by name + Create networks automatically + Not enough target space. Required %1$s, available %2$s. + Import folder + Disks will be imported to:\n%1$s + Imported VM: %1$s + Import complete + Import failed: %1$s + Not a valid vmpkg: %1$s From b4ccc68ed4dda03bf330555ad12687a9f89202ff Mon Sep 17 00:00:00 2001 From: BigfootACA Date: Tue, 7 Jul 2026 17:33:54 +0800 Subject: [PATCH 115/182] HugePageActivity: add kernel module unload button Signed-off-by: BigfootACA --- .../droidvm/ui/hugepage/HugePageActivity.java | 33 +++++++++++++++++++ .../droidvm/ui/hugepage/HugePageModel.java | 5 +++ app/src/main/res/drawable/ic_addon_remove.xml | 9 +++++ app/src/main/res/menu/menu_hugepage.xml | 11 +++++++ app/src/main/res/values-zh-rCN/strings.xml | 4 +++ app/src/main/res/values-zh-rTW/strings.xml | 4 +++ app/src/main/res/values/strings.xml | 4 +++ 7 files changed, 70 insertions(+) create mode 100644 app/src/main/res/drawable/ic_addon_remove.xml create mode 100644 app/src/main/res/menu/menu_hugepage.xml diff --git a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageActivity.java index f562b72de..4433432ce 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageActivity.java @@ -9,6 +9,7 @@ import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; import static cn.classfun.droidvm.lib.utils.StringUtils.pathJoin; import static cn.classfun.droidvm.lib.utils.ThreadUtils.runOnPool; +import static cn.classfun.droidvm.lib.ui.MaterialMenu.setupToolbarMenu; import android.content.Context; import android.content.Intent; @@ -146,6 +147,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { private void initialize() { toolbar.setTitle(R.string.hugepage_title); toolbar.setNavigationOnClickListener(v -> finish()); + setupToolbarMenu(toolbar, R.menu.menu_hugepage, this::onMenuItemClicked); btnSavePoolSize.setOnClickListener(v -> { if (moduleAcquiring) interruptAcquire(); else savePoolSize(); @@ -703,6 +705,37 @@ private void confirmUnload() { .show(); } + private boolean onMenuItemClicked(@NonNull android.view.MenuItem item) { + if (item.getItemId() == R.id.action_unload_module) { + confirmUnloadModule(); + return true; + } + return false; + } + + private void confirmUnloadModule() { + new MaterialAlertDialogBuilder(this) + .setTitle(R.string.hugepage_unload_module_title) + .setMessage(R.string.hugepage_unload_module_confirm) + .setPositiveButton(R.string.hugepage_unload_module, (d, w) -> doUnloadModule()) + .setNegativeButton(android.R.string.cancel, null) + .show(); + } + + private void doUnloadModule() { + btnModuleToggle.setEnabled(false); + runOnPool(() -> { + stopAcquireAndWait(); + var res = model.unload(); + runOnUiThread(() -> { + Toast.makeText(this, res.ok() + ? R.string.hugepage_unload_module_done + : R.string.hugepage_unload_failed, LENGTH_SHORT).show(); + refreshStatus(); + }); + }); + } + private void doDismissCrash() { runOnPool(() -> { runList("rm", "-f", CRASH_FILE); diff --git a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageModel.java b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageModel.java index 8d9a92e39..30b7fb4b8 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageModel.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageModel.java @@ -334,6 +334,11 @@ Result setRuntimeEnabled(boolean on) { return collapse(unloadLadder()); } + @NonNull + Result unload() { + return collapse(unloadLadder()); + } + /** * Enable (true) or disable (false) the module for the next boot via the * Magisk {@code disable} marker file. Independent of the running module. diff --git a/app/src/main/res/drawable/ic_addon_remove.xml b/app/src/main/res/drawable/ic_addon_remove.xml new file mode 100644 index 000000000..7bccaacc7 --- /dev/null +++ b/app/src/main/res/drawable/ic_addon_remove.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/menu/menu_hugepage.xml b/app/src/main/res/menu/menu_hugepage.xml new file mode 100644 index 000000000..25a62ca27 --- /dev/null +++ b/app/src/main/res/menu/menu_hugepage.xml @@ -0,0 +1,11 @@ + +

    + + + + diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 477f2f46d..b577e3fda 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -944,6 +944,10 @@ 确定要停止大页预留模块吗?这将释放所有预分配的页面,可能会影响正在运行的虚拟机。 模块已卸载 卸载模块失败 + 停止内核模块 + 确定停止内核模块? + 强制从运行中的内核移除 .ko(rmmod)。这将释放所有预留页面,并丢失每虚拟机跟踪,直到下次加载模块。可能会影响正在运行的虚拟机。 + 模块已卸载 允许通知? DroidVM 会在后台下载磁盘镜像,并通过通知显示进度。请允许通知,以便随时查看下载进度。 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 1f2e25709..d6d5a0956 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -973,6 +973,10 @@ 確定要停止大頁預留模組嗎?這將釋放所有預分配的頁面,可能會影響正在執行的虛擬機器。 模組已解除安裝 解除安裝模組失敗 + 停止內核模組 + 確定停止內核模組? + 強制從執行中的內核移除 .ko(rmmod)。這將釋放所有預留頁面,並丟失每虛擬機追蹤,直到下次載入模組。可能會影響正在執行的虛擬機器。 + 模組已解除安裝 顯示占用 Hugepage 集區占用 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0f264f433..85f08a344 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1010,6 +1010,10 @@ Are you sure you want to unload the hugepage reserve module? This will release all pre-allocated pages and may affect running VMs. Module unloaded Failed to unload module + Unload module + Unload module? + Forcibly remove the .ko from the running kernel (rmmod). This releases all reserved pages and loses per-VM tracking until next load. May affect running VMs. + Module unloaded Show Usage Hugepage Pool Usage From 164cf26de8c3fbe669254a191bfc4e19f01fb575 Mon Sep 17 00:00:00 2001 From: HuJK Date: Sun, 12 Jul 2026 23:51:27 +0800 Subject: [PATCH 116/182] SizeUtils: keep unit fallbacks inside the allowed unit list When no unit fits exactly (findUnit) or is at least the value (findFloatUnit), fall back to the smallest unit in the *provided* list rather than hardcoding SizeUnit.B, so a restricted picker (e.g. a GiB-only list) never yields a unit outside it. For the full list units[0] is B, so the classic behaviour is unchanged. Split out of the v10 CMA reservoir work as an independent change. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01ARourbDczkCYsKUUMC9EFv --- .../java/cn/classfun/droidvm/lib/size/SizeUtils.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/cn/classfun/droidvm/lib/size/SizeUtils.java b/app/src/main/java/cn/classfun/droidvm/lib/size/SizeUtils.java index 81cf70c8a..b5870fb2d 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/size/SizeUtils.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/size/SizeUtils.java @@ -20,7 +20,12 @@ public static SizeNumber findUnit( for (int i = units.length - 1; i >= 0; i--) if (units[i].fitsExactly(bytes)) return units[i].calcPair(bytes); - return SizeUnit.B.calcPair(bytes); + // Nothing fits exactly: fall back to the smallest *allowed* unit so a + // restricted list (e.g. a GiB-only picker) never yields a unit outside + // it. For the full list units[0] is B - identical to the old fallback. + return units.length > 0 + ? units[0].calcPair(new BigDecimal(bytes)) + : SizeUnit.B.calcPair(bytes); } @NonNull @@ -31,7 +36,10 @@ public static SizeNumber findFloatUnit( for (int i = units.length - 1; i >= 0; i--) if (units[i].isAtLeast(bytes)) return units[i].calcPair(bytes); - return SizeUnit.B.calcPair(bytes); + // See findUnit: keep the fallback inside the allowed list. + return units.length > 0 + ? units[0].calcPair(bytes) + : SizeUnit.B.calcPair(bytes); } @NonNull From 45d9155b720fedcd3d59b815d409ae44bd71771a Mon Sep 17 00:00:00 2001 From: HuJK Date: Sun, 12 Jul 2026 23:51:27 +0800 Subject: [PATCH 117/182] SwitchRowWidget: propagate setEnabled to the child switch FrameLayout.setEnabled alone left the child MaterialSwitch enabled and directly draggable, so a "disabled" row could still fire its change listener. Override setEnabled to propagate to the switch, and gate the row's click-to-toggle on switchView.isEnabled() so a disabled switch can't be toggled by tapping the row. Split out of the v10 CMA reservoir work as an independent change. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01ARourbDczkCYsKUUMC9EFv --- .../ui/widgets/row/SwitchRowWidget.java | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/cn/classfun/droidvm/ui/widgets/row/SwitchRowWidget.java b/app/src/main/java/cn/classfun/droidvm/ui/widgets/row/SwitchRowWidget.java index 5b4146671..5cd2dc8d6 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/widgets/row/SwitchRowWidget.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/widgets/row/SwitchRowWidget.java @@ -57,7 +57,9 @@ private void init(@Nullable AttributeSet attrs) { switchView = findViewById(R.id.sw_switch); initAttrs(attrs); if (isInEditMode()) return; - setOnClickListener(v -> switchView.toggle()); + setOnClickListener(v -> { + if (switchView.isEnabled()) switchView.toggle(); + }); } private void initAttrs(@Nullable AttributeSet attrs) { @@ -94,10 +96,23 @@ public boolean isChecked() { @SuppressLint("ClickableViewAccessibility") public void setSwitchEnabled(boolean enabled) { switchView.setOnTouchListener(enabled ? null : (v, e) -> true); - setOnClickListener(enabled ? v -> switchView.toggle() : null); + setOnClickListener(!enabled ? null : v -> { + if (switchView.isEnabled()) switchView.toggle(); + }); setClickable(enabled); } + /** + * FrameLayout.setEnabled alone leaves the child MaterialSwitch enabled and + * directly draggable, so a "disabled" row could still fire its change + * listener. Propagate to the switch so disabling actually greys and locks it. + */ + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + switchView.setEnabled(enabled); + } + public void setChecked(boolean checked) { switchView.setChecked(checked); } From fed2e083f7480ffd2a35a4abdd26db05104f82af Mon Sep 17 00:00:00 2001 From: HuJK Date: Sun, 12 Jul 2026 23:51:54 +0800 Subject: [PATCH 118/182] TextInputRowWidget: add fixed-unit (ti_unit) and hide-unit (ti_showUnit) Two new size-mode options: - ti_unit pins the picker to exactly one unit (e.g. "GiB"): values always display and are entered in it, no other unit is offered. - ti_showUnit hides the picker button in narrow layouts while the fixed/ derived unit still applies to typed values; setUnitButtonVisible toggles it at runtime. Split out of the v10 CMA reservoir work as an independent change. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01ARourbDczkCYsKUUMC9EFv --- .../ui/widgets/row/TextInputRowWidget.java | 58 ++++++++++++++++++- app/src/main/res/values/attrs.xml | 6 ++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/cn/classfun/droidvm/ui/widgets/row/TextInputRowWidget.java b/app/src/main/java/cn/classfun/droidvm/ui/widgets/row/TextInputRowWidget.java index f3951fbd6..03847de12 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/widgets/row/TextInputRowWidget.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/widgets/row/TextInputRowWidget.java @@ -1,5 +1,7 @@ package cn.classfun.droidvm.ui.widgets.row; +import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; + import android.content.Context; import android.text.Editable; import android.text.InputFilter; @@ -48,6 +50,9 @@ public final class TextInputRowWidget extends FrameLayout { private BigInteger minValue = BigInteger.ZERO; private BigInteger maxValue = BigInteger.valueOf(Long.MAX_VALUE); private BigInteger precision = BigInteger.ONE; + @Nullable + private SizeUnit fixedUnit = null; // ti_unit: pin size mode to this one unit + private boolean showUnitButton = true; // ti_showUnit: hide the picker button private final AtomicBoolean updatingValue = new AtomicBoolean(false); private Runnable onFocusLostListener; @@ -134,6 +139,14 @@ private void initAttrs(@Nullable AttributeSet attrs) { iconButtonView.setVisibility(GONE); } var tiMode = a.getInt(R.styleable.TextInputRowWidget_ti_mode, MODE_NORMAL); + var unit = a.getString(R.styleable.TextInputRowWidget_ti_unit); + if (unit != null) { + fixedUnit = SizeUnit.fromString(unit); + if (fixedUnit == null) + throw new IllegalArgumentException(fmt("Unknown ti_unit: %s", unit)); + } + showUnitButton = a.getBoolean( + R.styleable.TextInputRowWidget_ti_showUnit, true); var min = a.getString(R.styleable.TextInputRowWidget_ti_min); var max = a.getString(R.styleable.TextInputRowWidget_ti_max); if (!isInEditMode()) { @@ -150,6 +163,33 @@ private void initAttrs(@Nullable AttributeSet attrs) { } } + /** + * Fixed-unit variant of {@link #setPickerByMinMax}: min/max still bound the + * byte value, but the picker offers exactly {@code fixedUnit} - the button + * degrades to a unit tag (ROTATE over one item is a no-op). + */ + private void setPickerFixedUnit( + @NonNull SizeUnit unit, + @Nullable String min, + @Nullable String max + ) { + if (min != null) { + var parsed = SizeUtils.parseBigSize(min); + if (parsed.compareTo(BigInteger.ZERO) < 0) + throw new IllegalArgumentException("Min value must be non-negative"); + minValue = parsed; + } + if (max != null) { + var parsed = SizeUtils.parseBigSize(max); + if (parsed.compareTo(BigInteger.ZERO) < 0) + throw new IllegalArgumentException("Max value must be non-negative"); + maxValue = parsed; + } + if (minValue.compareTo(maxValue) > 0) + throw new IllegalArgumentException("Min value cannot be greater than max value"); + buttonView.setItems(unit); + } + private void setPickerByMinMax( @Nullable String min, @Nullable String max @@ -196,10 +236,13 @@ private void applyMode(int mode, @Nullable String min, @Nullable String max) { } else if (mode == MODE_SIZE) { var flags = InputType.TYPE_CLASS_NUMBER; flags |= InputType.TYPE_NUMBER_FLAG_DECIMAL; - buttonView.setVisibility(VISIBLE); + // The picker is still configured when hidden - the (fixed) unit + // keeps applying to typed values; only the button goes away. + buttonView.setVisibility(showUnitButton ? VISIBLE : GONE); if (!isInEditMode()) { buttonView.setMode(PickerButtonWidget.Mode.ROTATE); - setPickerByMinMax(min, max); + if (fixedUnit != null) setPickerFixedUnit(fixedUnit, min, max); + else setPickerByMinMax(min, max); } editText.setInputType(flags); } @@ -320,6 +363,17 @@ public void setSelection(int index) { editText.setSelection(index); } + /** + * Show/hide the size-mode unit button at runtime (see {@code ti_showUnit}). + * The picker stays configured either way - the unit keeps applying to + * typed values; only the button's width comes and goes. + */ + public void setUnitButtonVisible(boolean visible) { + showUnitButton = visible; + if (mode == MODE_SIZE) + buttonView.setVisibility(visible ? VISIBLE : GONE); + } + public void setError(@Nullable CharSequence error) { textInputLayout.setError(error); } diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index e431d4cc5..48e7231a4 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -72,6 +72,12 @@ + + + + From 7a61e668b87c12d2ba0ae101a3514f3981650009 Mon Sep 17 00:00:00 2001 From: HuJK Date: Sun, 12 Jul 2026 23:59:06 +0800 Subject: [PATCH 119/182] hugepage: add v10 CMA reservoir management Manage the module's CMA reservoir (pool_want_with_cma) from the hugepage screen, and fix the pieces of the existing UI that the second target broke. CMA switch + consumability probe: - A switch above "Enable Module" reads cma_probe_result from settings.prop: a recorded pass enables directly, anything else offers the probe. - The probe raises pool_want_with_cma (never lowers an existing bigger total - that would demolish reservoir the device is lending out), then empties the pool with a live pool_want=0 so its blocks flip straight into the reservoir, and lets a v3 acquire top it up as far as it can. - Coming up short is not a distinct failure: the reservoir is assembled out of the pool itself, so a fresh boot - unfragmented - simply succeeds. - pool_want is only ever written live, so an app killed mid-probe cannot leave the pool soft-disabled past a reboot; every exit restores it. - A pass pins the pool to 512 MB and keeps the rest as reservoir; a denial or unreadable result is put to the user. settings.prop writes became a locked read-modify-write so the probe thread and the pool-size save can't wipe each other's keys. Runtime insmod now matches the boot-time one: prefer the module's own load.sh (single source of truth for the preflight), else reconstruct only v9's kapi_check ABI guard. insmod args are joined via StringUtils.joinNonEmpty. Bar and captions: [VMs][available][CMA free][CMA lent][waiting], denominated by pool_want_with_cma with the reservoir counted as filled. SegmentedBar's storage bar takes a StorageSpec holder instead of an 18-argument call. Snapshot parses a refill_stat key=value map in a dedicated constructor, with field defaults standing in for the not-loaded view. Acquire gating follows the module's own rule (acquire_set): work exists when the pool is short OR the reservoir is short. A CMA-era grow kicks v3, not v1. Pool size is a GiB-only input (ti_unit), split into pool / with-CMA total when the reservoir is on, two-way linked so the pair keeps pool_want <= pool_want_with_cma. VM bar colours are keyed by rank among the live pids rather than by pid. Co-Authored-By: lateautumn233 Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01ARourbDczkCYsKUUMC9EFv --- .../droidvm/lib/utils/StringUtils.java | 12 + .../droidvm/ui/hugepage/HugePageActivity.java | 994 +++++++++++++++++- .../droidvm/ui/hugepage/HugePageColor.java | 65 +- .../droidvm/ui/hugepage/HugePageModel.java | 521 +++++++-- .../droidvm/ui/hugepage/HugePageProcess.java | 27 +- .../ui/hugepage/HugePageProcessActivity.java | 107 +- .../ui/hugepage/HugePageProcessAdapter.java | 15 +- .../droidvm/ui/hugepage/SegmentedBar.java | 123 ++- app/src/main/res/layout/activity_hugepage.xml | 51 +- app/src/main/res/values-night/colors.xml | 3 + app/src/main/res/values-zh-rCN/strings.xml | 49 + app/src/main/res/values-zh-rTW/strings.xml | 49 + app/src/main/res/values/colors.xml | 5 + app/src/main/res/values/strings.xml | 49 + 14 files changed, 1893 insertions(+), 177 deletions(-) diff --git a/app/src/main/java/cn/classfun/droidvm/lib/utils/StringUtils.java b/app/src/main/java/cn/classfun/droidvm/lib/utils/StringUtils.java index 3d49a8c9f..649dbc101 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/utils/StringUtils.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/utils/StringUtils.java @@ -189,6 +189,18 @@ public static String pathJoin(@NonNull File base, @NonNull String... children) { return pathJoin(base.getAbsolutePath(), children); } + /** Join the non-empty parts with {@code sep}; empty parts are skipped. */ + @NonNull + public static String joinNonEmpty(@NonNull String sep, @NonNull String... parts) { + var sb = new StringBuilder(); + for (var p : parts) { + if (p.isEmpty()) continue; + if (sb.length() > 0) sb.append(sep); + sb.append(p); + } + return sb.toString(); + } + @NonNull public static String fmt(String fmt, Object... args) { return new Formatter(Locale.ROOT).format(fmt, args).toString(); diff --git a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageActivity.java index 4433432ce..66b7bd6ea 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageActivity.java @@ -38,6 +38,10 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.progressindicator.LinearProgressIndicator; +import android.text.Editable; +import android.widget.LinearLayout; +import android.widget.ProgressBar; + import java.math.BigInteger; import java.util.ArrayList; import java.util.LinkedHashMap; @@ -45,10 +49,13 @@ import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import cn.classfun.droidvm.R; import cn.classfun.droidvm.lib.daemon.DaemonConnection; import cn.classfun.droidvm.lib.size.SizeUtils; +import cn.classfun.droidvm.lib.ui.SimpleTextWatcher; import cn.classfun.droidvm.ui.widgets.row.SwitchRowWidget; import cn.classfun.droidvm.ui.widgets.row.TextInputRowWidget; import cn.classfun.droidvm.ui.widgets.row.TextRowWidget; @@ -102,6 +109,43 @@ public final class HugePageActivity extends AppCompatActivity { private TextView tvPoolTotal; private TextView tvPoolSize; private SwitchRowWidget rowModuleEnable; + // v10 CMA reservoir controls. The right input is the TOTAL with-CMA pool + // size (pool_want_with_cma), not the reservoir delta. + private TextInputRowWidget inputCmaSize; + private SwitchRowWidget rowCmaEnable; + private boolean cmaSwitchSyncing = false; // programmatic setChecked guard + private boolean cmaInputLoaded = false; // seed the CMA size input once per show + private boolean cmaBusy = false; // a probe / toggle flow is in flight + // A reservoir target persisted with no verdict recorded = a probe that asked + // for a reboot and is now waiting to be finished. Offer it once per screen. + private boolean probePromptShown = false; + // Two-way size link (pool_want <= pool_want_with_cma): which input the + // user touched last decides who yields when they cross. + private static final int SIZE_EDIT_POOL = 1; + private static final int SIZE_EDIT_CMA = 2; + private int lastSizeEdit = SIZE_EDIT_POOL; + private boolean sizeLinkSyncing = false; // programmatic setBigValue guard + /** Balloon floor (MB) for the consumability probe - `balloon 1536`. */ + private static final long BALLOON_FLOOR_MB = 1536; + private static final long BALLOON_TIMEOUT_S = 600; + /** + * How much reservoir the probe wants to measure against: + * {@code max(RAM - 8G, RAM * 0.4)}. RAM here is MemTotal - physical pages + * only, so zram/swap capacity (SwapTotal) never inflates it. Advisory, not + * a precondition: a smaller reservoir still probes, it just makes the + * verdict less reliable, and the user is warned before continuing. + */ + private static final long PROBE_KEEP_BYTES = 8L << 30; // 8 GiB + private static final double PROBE_MIN_RAM_FRACTION = 0.4; + /** + * Pool size a passing probe leaves behind. The probe has just proved apps + * can consume the reservoir, so holding a large pool would waste memory the + * reservoir would otherwise lend out: pin the pool small and let + * pool_want_with_cma (kept at the size the probe assembled) carry the rest + * as reservoir. A VM start stages pages back in on demand. + */ + private static final long PROBE_POOL_BYTES = 512L << 20; // 512 MiB + private static final long PROBE_POOL_PAGES = PROBE_POOL_BYTES / PAGE_SIZE; private TextRowWidget rowStatState; private TextRowWidget rowStatTotalServed; private TextRowWidget rowStatTotalRefilled; @@ -137,6 +181,8 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { tvPoolTotal = findViewById(R.id.tv_pool_total); tvPoolSize = findViewById(R.id.tv_pool_size); rowModuleEnable = findViewById(R.id.row_module_enable); + inputCmaSize = findViewById(R.id.input_cma_size); + rowCmaEnable = findViewById(R.id.row_cma_enable); rowStatState = findViewById(R.id.row_stat_state); rowStatTotalServed = findViewById(R.id.row_stat_total_served); rowStatTotalRefilled = findViewById(R.id.row_stat_total_refilled); @@ -153,6 +199,24 @@ private void initialize() { else savePoolSize(); }); rowModuleEnable.setOnCheckedChangeListener(this::doToggleModule); + rowCmaEnable.setOnCheckedChangeListener((btn, checked) -> onCmaSwitchChanged(checked)); + // Two-way link between the pool size and the with-CMA total: track who + // was edited last, reconcile whenever a field is left (and again at + // save, since tapping Save doesn't steal the EditText's focus). + inputPoolSize.addTextChangedListener(new SimpleTextWatcher() { + @Override + public void afterTextChanged(Editable s) { + if (!sizeLinkSyncing) lastSizeEdit = SIZE_EDIT_POOL; + } + }); + inputCmaSize.addTextChangedListener(new SimpleTextWatcher() { + @Override + public void afterTextChanged(Editable s) { + if (!sizeLinkSyncing) lastSizeEdit = SIZE_EDIT_CMA; + } + }); + inputPoolSize.setOnFocusLostListener(this::reconcileSizeLink); + inputCmaSize.setOnFocusLostListener(this::reconcileSizeLink); // One button: // not installed -> Install (open releases page) // installed, unloaded -> Enable (insmod) @@ -255,6 +319,42 @@ static void showAcquireInfo(@NonNull Context ctx, int mode, @NonNull Runnable on .show(); } + /** + * The "acquire finished" bubble, shared by both hugepage screens: how much + * the pool reached of its target, and - while the v10 reservoir is on - the + * with-CMA total too. Both matter because acquire's own stop condition + * covers both (see {@link HugePageModel.Snapshot#deficit}): a pool that hit + * its target while the reservoir is still short is not "complete", and a + * grown pool_want is filled by staging reservoir pages in, which moves the + * pool number without moving the total. + */ + @NonNull + static String acquireDoneMessage( + @NonNull Context ctx, @NonNull HugePageModel.Snapshot snap + ) { + long gotPool = snap.free + snap.lent; + long wantPool = snap.targetIdeal; + if (!snap.cmaActive()) { + return gotPool >= wantPool + ? ctx.getString(R.string.hugepage_proc_acquire_full, pageSize(wantPool)) + : ctx.getString(R.string.hugepage_proc_acquire_partial, + pageSize(gotPool), pageSize(wantPool)); + } + long gotTotal = gotPool + snap.cmaPool; + long wantTotal = snap.wantWithCma; + return (gotPool >= wantPool && gotTotal >= wantTotal) + ? ctx.getString(R.string.hugepage_proc_acquire_full_cma, + pageSize(wantPool), pageSize(wantTotal)) + : ctx.getString(R.string.hugepage_proc_acquire_partial_cma, + pageSize(gotPool), pageSize(wantPool), + pageSize(gotTotal), pageSize(wantTotal)); + } + + @NonNull + private static String pageSize(long pages) { + return SizeUtils.formatSize(pages * PAGE_SIZE); + } + /** True once the user opted out of the acquire prompt (short-press runs directly). */ static boolean skipAcquireConfirm(@NonNull Context ctx) { return ctx.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) @@ -345,16 +445,29 @@ private void refreshStatus() { // Per-VM breakdown through the usage ladder (KO attribution, degrading // to a THP scan of running VMs), so this bar matches the usage screen. // Each segment is labelled with the friendly VM name from vmMap. + // allPids is the unfiltered owner list - the rank-based color map + // must see every pid (both screens derive ranks from the same list). List owners = new ArrayList<>(); + List allPids = new ArrayList<>(); if (snap.loaded) { for (var e : model.usage(null).entries) { + allPids.add(e.pid); if (e.pages > 0) owners.add(new long[]{e.pid, e.pages}); } } // Only fetch VM names when there are rows to label. Map vmMap = owners.isEmpty() ? new LinkedHashMap<>() : model.vmNames(false); - runOnUiThread(() -> updateUI(snap, crashStamp, owners, vmMap)); + // Reservoir occupancy for the two-tone CMA block (module caches ~1s). + var cmaUsage = snap.cmaActive() ? model.cmaUsage() : null; + // A reservoir built (or being built) toward a target nobody ever + // judged: the "save and reboot" branch of the probe left it here. + boolean probePending = !probePromptShown && !cmaBusy + && snap.cmaActive() && model.cmaProbeResult() == null; + runOnUiThread(() -> { + updateUI(snap, crashStamp, owners, allPids, vmMap, cmaUsage); + if (probePending) promptPendingProbe(); + }); }); } @@ -376,7 +489,9 @@ private void setPagesString(@NonNull TextView tv, @StringRes int str, long pages private void updateUI( @NonNull HugePageModel.Snapshot snap, boolean crashed, @NonNull List owners, - @NonNull Map vmMap + @NonNull List allPids, + @NonNull Map vmMap, + @Nullable HugePageModel.CmaUsage cmaUsage ) { if (isFinishing()) return; cardCrashWarning.setVisibility(crashed ? VISIBLE : GONE); @@ -390,11 +505,16 @@ private void updateUI( // "total" shows the desired target - the model's version-unified // want (pool_want, else v6 pool_target, else current capacity). var poolWant = snap.targetIdeal; + boolean cmaOn = snap.cmaActive(); // Apple-storage-bar style: one labelled colored block per VM // (used), then the available portion as a track-coloured gap, - // then the waiting-to-acquire (deficit) block pinned flush right. - // Each block draws its label inside if wide enough. + // then (v10) the CMA reservoir split into occupied-by-apps and + // free halves, then the waiting-to-acquire (deficit) block pinned + // flush right. Each block draws its label inside if wide enough. boolean dark = HugePageColor.isDark(this); + // Rank-based colors over the full owner list, so adjacent VM + // segments never land on near-identical hues (see HugePageColor). + var colorMap = HugePageColor.forPids(allPids, dark); int n = owners.size(); int[] usedColors = new int[n]; float[] usedValues = new float[n]; @@ -403,7 +523,8 @@ private void updateUI( for (int i = 0; i < n; i++) { int pid = (int) owners.get(i)[0]; long ownerPages = owners.get(i)[1]; - usedColors[i] = HugePageColor.forPid(pid, dark); + Integer color = colorMap.get(pid); + usedColors[i] = color != null ? color : HugePageColor.forRank(i, dark); usedValues[i] = ownerPages; String name = vmMap.get(pid); if (name == null) name = getString(R.string.hugepage_proc_pid, pid); @@ -417,21 +538,68 @@ private void updateUI( // orphaned owner-gone pages that have no segment; those surface in // the held/available gap rather than as an invisible caption delta. long used = seg; - long deficit = Math.max(0, poolWant - seg - poolAvail); + // With the reservoir on, the bar's denominator is the overall + // target pool_want_with_cma and the reservoir counts as filled. + long cmaPool = cmaOn ? snap.cmaPool : 0; + long barWant = cmaOn ? snap.wantWithCma : poolWant; + long deficit = Math.max(0, barWant - seg - poolAvail - cmaPool); + // Reservoir occupancy split (pages): still-free vs held by other + // apps right now; unknown occupancy shows one undivided free block. + boolean cmaUsageOk = cmaOn && cmaUsage != null && cmaUsage.ok; + long cmaOther = cmaUsageOk + ? Math.min(cmaPool, cmaUsage.usedMb / (PAGE_SIZE / (1024 * 1024))) + : 0; + long cmaFree = cmaPool - cmaOther; + // Avail sub-split: pages flippable to CMA as whole pageblocks vs + // not (pool_avail_cma_able); -1 = unreported, no split shown. + long availCmaAble = (cmaOn && snap.availCmaAble >= 0) + ? Math.min(poolAvail, snap.availCmaAble) : -1; + long availNonCma = availCmaAble >= 0 ? poolAvail - availCmaAble : 0; // 2x2 caption: used / available on top, total / pool-size below. - // Total = real held reserve (used + avail), shown raw - no clamp - // to the pool size, so a kernel that fails to release on shrink - // shows up as total > the size you set. + // With the reservoir on, the pool-size cell shows both targets as + // pool_want/pool_want_with_cma; the detailed cma-able and + // free/other-apps breakdowns live on the usage screen's synthetic + // available/CMA rows. Total = real held reserve (used + avail), + // shown raw - no clamp to the pool size, so a kernel that fails + // to release on shrink shows up as total > the size you set. var held = used + poolAvail; setPagesString(tvPoolUsed, R.string.hugepage_stat_pool_used, used); setPagesString(tvPoolAvail, R.string.hugepage_stat_pool_available, poolAvail); setPagesString(tvPoolTotal, R.string.hugepage_stat_pool_total, held); - setPagesString(tvPoolSize, R.string.hugepage_stat_pool_size, poolWant); - segPoolBar.setStorage(usedColors, usedValues, usedLabels, - poolAvail, fmt("%s\n%s", getString(R.string.hugepage_bar_available), SizeUtils.formatSize(poolAvail * PAGE_SIZE)), - HugePageColor.pending(this), deficit, - fmt("%s\n%s", getString(R.string.hugepage_proc_deficit), SizeUtils.formatSize(deficit * PAGE_SIZE)), - poolWant); + if (cmaOn) { + tvPoolSize.setText(getString(R.string.hugepage_stat_pool_size_cma, + poolWant, snap.wantWithCma, + SizeUtils.formatSize(poolWant * PAGE_SIZE), + SizeUtils.formatSize(snap.wantWithCma * PAGE_SIZE))); + } else { + setPagesString(tvPoolSize, R.string.hugepage_stat_pool_size, poolWant); + } + // Bar: [VMs][avail][CMA][CMA lent][waiting]. The CMA parts are two + // ordinary labelled segments; only the avail block keeps the + // single-label pure-color sub-split ([non-cma-able|normal]). + var spec = new SegmentedBar.StorageSpec(); + spec.usedColors = usedColors; + spec.usedValues = usedValues; + spec.usedLabels = usedLabels; + spec.avail = poolAvail; + spec.availLabel = fmt("%s\n%s", getString(R.string.hugepage_bar_available), + SizeUtils.formatSize(poolAvail * PAGE_SIZE)); + spec.availNonCma = Math.max(0, availNonCma); + spec.availNonCmaColor = HugePageColor.availNonCma(this); + spec.cmaFree = cmaFree; + spec.cmaFreeLabel = fmt("%s\n%s", getString(R.string.hugepage_bar_cma), + SizeUtils.formatSize(cmaFree * PAGE_SIZE)); + spec.cmaFreeColor = HugePageColor.cmaFree(this); + spec.cmaOther = cmaOther; + spec.cmaOtherLabel = fmt("%s\n%s", getString(R.string.hugepage_bar_cma_lent), + SizeUtils.formatSize(cmaOther * PAGE_SIZE)); + spec.cmaOtherColor = HugePageColor.cmaUsed(this); + spec.deficitColor = HugePageColor.pending(this); + spec.deficit = deficit; + spec.deficitLabel = fmt("%s\n%s", getString(R.string.hugepage_proc_deficit), + SizeUtils.formatSize(deficit * PAGE_SIZE)); + spec.want = barWant; + segPoolBar.setStorage(spec); } else { rowStatState.setValue(getString(R.string.hugepage_stats_unavailable)); rowStatTotalServed.setValue(null); @@ -459,14 +627,7 @@ poolAvail, fmt("%s\n%s", getString(R.string.hugepage_bar_available), SizeUtils.f // which polls). Track the kernel flag, not the optimistic mainAcquiring, so // an acquire that never actually started can't fake a "done". if (wasAcquiring && !acquiring) { - long got = snap.free + snap.lent; - long want = snap.targetIdeal; - String msg = got >= want - ? getString(R.string.hugepage_proc_acquire_full, - SizeUtils.formatSize(want * PAGE_SIZE)) - : getString(R.string.hugepage_proc_acquire_partial, - SizeUtils.formatSize(got * PAGE_SIZE), - SizeUtils.formatSize(want * PAGE_SIZE)); + String msg = acquireDoneMessage(this, snap); // Append why the acquire stopped (kernel free text from refill_stat's // acquire_stop_reason), so the user sees the reason in the bubble. String reason = snap.acquireStopReason; @@ -511,6 +672,40 @@ poolAvail, fmt("%s\n%s", getString(R.string.hugepage_bar_available), SizeUtils.f // soft-disable (want 0), so there is no version branching here. acquireEnabled = snap.loaded && snap.deficit > 0; applyAcquireState(); + + // CMA switch: only meaningful on a loaded v10 module. While a probe / + // toggle flow runs, leave the switch and the size input alone - the flow + // owns them (the reservoir flips around mid-probe and would flicker). + rowCmaEnable.setEnabled(snap.loaded && snap.hasCma); + if (!cmaBusy) { + boolean cmaActive = snap.cmaActive(); + if (rowCmaEnable.isChecked() != cmaActive) { + cmaSwitchSyncing = true; + rowCmaEnable.setChecked(cmaActive); + cmaSwitchSyncing = false; + } + // The size row shows exactly one GiB tag: on the right field while + // CMA is on (two fields, tight width), on the pool field otherwise. + inputPoolSize.setUnitButtonVisible(!cmaActive); + if (cmaActive) { + // Seed the with-CMA total input once per show (it maps 1:1 to + // pool_want_with_cma), then leave the user's typing be. + if (inputCmaSize.getVisibility() != VISIBLE || !cmaInputLoaded) { + inputCmaSize.setVisibility(VISIBLE); + sizeLinkSyncing = true; + try { + inputCmaSize.setBigValue( + BigInteger.valueOf(snap.wantWithCma * PAGE_SIZE)); + } finally { + sizeLinkSyncing = false; + } + cmaInputLoaded = true; + } + } else { + inputCmaSize.setVisibility(GONE); + cmaInputLoaded = false; + } + } } /** @@ -599,7 +794,16 @@ private void loadPoolSize() { try { var pages = Long.parseLong(cur); var bytes = BigInteger.valueOf(pages * PAGE_SIZE); - runOnUiThread(() -> inputPoolSize.setBigValue(bytes)); + runOnUiThread(() -> { + // Programmatic seed - don't count it as a user edit for + // the pool<->with-CMA size link. + sizeLinkSyncing = true; + try { + inputPoolSize.setBigValue(bytes); + } finally { + sizeLinkSyncing = false; + } + }); } catch (NumberFormatException e) { Log.w(TAG, "Failed to parse pool_want", e); } @@ -643,10 +847,42 @@ private long runningVmMemMib() { return total[0]; } + /** + * Keep the size pair consistent ({@code pool_want <= pool_want_with_cma}): + * when they cross, the field the user touched last wins - raising the pool + * above the total drags the total up; lowering the total under the pool + * shrinks the pool. Runs on focus-loss of either field and again at save. + */ + private void reconcileSizeLink() { + if (inputCmaSize.getVisibility() != VISIBLE) return; + if (!inputPoolSize.isInputValid() || !inputCmaSize.isInputValid()) return; + var pool = inputPoolSize.getBigValue(); + var withCma = inputCmaSize.getBigValue(); + if (pool.compareTo(withCma) <= 0) return; + sizeLinkSyncing = true; + try { + if (lastSizeEdit == SIZE_EDIT_CMA) inputPoolSize.setBigValue(withCma); + else inputCmaSize.setBigValue(pool); + } finally { + sizeLinkSyncing = false; + } + } + private void savePoolSize() { + reconcileSizeLink(); if (!inputPoolSize.isInputValid()) return; var bytes = inputPoolSize.getBigValue(); var pages = bytes.divide(BigInteger.valueOf(PAGE_SIZE)); + // While the reservoir is on, the right field IS the with-CMA total + // (pool_want_with_cma); the link above already keeps it >= the pool. + final long cmaPages; + if (inputCmaSize.getVisibility() == VISIBLE) { + if (!inputCmaSize.isInputValid()) return; + cmaPages = inputCmaSize.getBigValue() + .divide(BigInteger.valueOf(PAGE_SIZE)).longValue(); + } else { + cmaPages = -1; + } runOnPool(() -> { // The pool must be able to back every running VM's RAM, so it can't // be set below the sum of running VMs' configured memory. @@ -661,12 +897,17 @@ private void savePoolSize() { } // Persist for the next load and apply to the running pool where the // live knob exists (v7); v6's read-only target only lands next boot. - var res = model.saveSize(pages.longValue()); - // A grow leaves a deficit -> kick one v1 fill so the raised target + // One settings.prop rewrite carries the pool target and (while the + // reservoir is on) the with-CMA total together. + var res = model.saveTargets(pages.longValue(), cmaPages); + // A grow leaves a deficit -> kick one fill so the raised target // starts filling at once (a shrink is applied by the write itself). - // The GUI drives acquire; the model's saveSize deliberately doesn't. + // The GUI drives acquire; the model's save deliberately doesn't. + // Only the mode-2/3 sweep runs the reservoir-building Phase R + // ("mode 1 remains pool-only legacy"), so a CMA-era grow needs v3. var snap = model.state(); - if (res.ok() && snap.loaded && snap.deficit > 0) model.acquire(1); + if (res.ok() && snap.loaded && snap.deficit > 0) + model.acquire(snap.cmaActive() ? 3 : 1); boolean okSaved = res.ok(); boolean appliedNow = "pool_want".equals(res.impl); // live write actually landed runOnUiThread(() -> { @@ -696,6 +937,703 @@ private void doToggleModule() { }); } + /* ================================================================== */ + /* v10 CMA reservoir: switch + consumability probe */ + /* ================================================================== */ + + private void onCmaSwitchChanged(boolean checked) { + if (cmaSwitchSyncing) return; + if (cmaBusy) { // a flow already owns the switch + setCmaSwitch(!checked); + return; + } + if (checked) doCmaEnable(); + else doCmaDisable(); + } + + /** Programmatic switch write that doesn't re-enter the change listener. */ + private void setCmaSwitch(boolean checked) { + cmaSwitchSyncing = true; + rowCmaEnable.setChecked(checked); + cmaSwitchSyncing = false; + } + + /** End an enable flow without enabling: release the busy lock, switch off. */ + private void cancelCmaEnable() { + cmaBusy = false; + setCmaSwitch(false); + refreshStatus(); + } + + /** Switch off: demolish the reservoir now and persist off for next boot. */ + private void doCmaDisable() { + cmaBusy = true; + runOnPool(() -> { + var res = model.saveCmaTarget(0); + runOnUiThread(() -> { + cmaBusy = false; + Toast.makeText(this, res.ok() ? R.string.hugepage_cma_disabled + : R.string.hugepage_cma_toggle_failed, LENGTH_SHORT).show(); + if (!res.ok()) setCmaSwitch(true); + refreshStatus(); + }); + }); + } + + /** + * Switch on. The magisk-side {@code cma_probe_result} (settings.prop) says + * whether the consumability probe ever ran: + *
      + *
    • {@code 1} - apps can consume CMA: enable directly, no probe;
    • + *
    • {@code 0} - probed unusable: offer a re-probe;
    • + *
    • absent - never probed: explain and offer to run it.
    • + *
    + */ + private void doCmaEnable() { + cmaBusy = true; + runOnPool(() -> { + var snap = model.state(); + if (!snap.loaded || !snap.hasCma) { + runOnUiThread(() -> { + cmaBusy = false; + setCmaSwitch(false); + Toast.makeText(this, R.string.hugepage_cma_not_supported, + LENGTH_SHORT).show(); + }); + return; + } + var verdict = model.cmaProbeResult(); + if (snap.cmaPbOrder < 0) { + // The module disabled its whole CMA side this boot (preflight / + // symbols / first-block verification) - no write can help now. + // A recorded denial is one of the causes (the boot script then + // hands the module -1 preflight values): drop it, so the next + // boot comes up CMA-capable and the probe can run again. + boolean stale = verdict != null && verdict == VERDICT_DENIED; + if (stale) model.clearCmaProbeResult(); + runOnUiThread(() -> { + cmaBusy = false; + setCmaSwitch(false); + if (isFinishing()) return; + new MaterialAlertDialogBuilder(this) + .setTitle(R.string.hugepage_cma_unavailable_title) + .setMessage(stale ? R.string.hugepage_cma_unavailable_denied + : R.string.hugepage_cma_unavailable_boot) + .setPositiveButton(android.R.string.ok, null) + .show(); + }); + return; + } + // The threshold only feeds the two dialog branches - don't pay the + // meminfo shell read when the verdict lets us enable directly. + var needBytes = (verdict != null && verdict == VERDICT_ALLOWED) + ? 0 : probeNeedBytes(model.memTotalKb()); + runOnUiThread(() -> { + if (isFinishing()) { + cmaBusy = false; + return; + } + if (verdict != null && verdict == VERDICT_ALLOWED) { + enableCmaDirect(snap); + } else if (verdict != null && verdict == VERDICT_DENIED) { + new MaterialAlertDialogBuilder(this) + .setTitle(R.string.hugepage_cma_probe_denied_title) + .setMessage(R.string.hugepage_cma_probe_denied_msg) + .setPositiveButton(R.string.hugepage_cma_probe_rerun, + (d, w) -> startCmaProbe()) + .setNegativeButton(android.R.string.cancel, + (d, w) -> cancelCmaEnable()) + .setOnCancelListener(d -> cancelCmaEnable()) + .show(); + } else { + new MaterialAlertDialogBuilder(this) + .setTitle(R.string.hugepage_cma_probe_needed_title) + .setMessage(getString(R.string.hugepage_cma_probe_needed_msg, + SizeUtils.formatSize(Math.max(0, needBytes)), + SizeUtils.formatSize(PROBE_POOL_BYTES))) + .setPositiveButton(R.string.hugepage_cma_probe_start, + (d, w) -> startCmaProbe()) + .setNegativeButton(android.R.string.cancel, + (d, w) -> cancelCmaEnable()) + .setOnCancelListener(d -> cancelCmaEnable()) + .show(); + } + }); + }); + } + + /** + * Probe already passed: restore the remembered with-CMA total and let a v3 + * acquire build the reservoir (only the mode-2/3 sweep runs Phase R; mode 1 + * is pool-only legacy). + * + *

    The target is clamped to the module's {@code pool_want <= + * pool_want_with_cma} invariant, so a remembered total at or below the pool + * size enables with an empty reservoir - that is a real state (the probe + * itself produces it when the pool already holds everything), and the user + * then raises the now-visible with-CMA field. Only {@code 0} is impossible: + * it is the off sentinel. + */ + private void enableCmaDirect(@NonNull HugePageModel.Snapshot snap) { + runOnPool(() -> { + long target = Math.max(model.lastCmaTargetPages(), snap.targetIdeal); + if (target <= 0) // pool soft-disabled: fall back to the probe floor + target = pagesFor(Math.max(0, probeNeedBytes(model.memTotalKb()))); + boolean ok = target > 0 && model.saveCmaTarget(target).ok(); + if (ok) { + var s2 = model.state(); + if (s2.loaded && s2.deficit > 0) model.acquire(3); + } + boolean fOk = ok; + runOnUiThread(() -> { + cmaBusy = false; + Toast.makeText(this, fOk ? R.string.hugepage_cma_enabled + : R.string.hugepage_cma_toggle_failed, LENGTH_SHORT).show(); + if (!fOk) setCmaSwitch(false); + cmaInputLoaded = false; // reseed the CMA size input + refreshStatus(); + }); + }); + } + + /* ---- probe orchestration ---- */ + + /** Progress dialog handle for the probe worker thread. */ + private static final class ProbeUi { + @NonNull final androidx.appcompat.app.AlertDialog dialog; + @NonNull final TextView text; + + ProbeUi(@NonNull androidx.appcompat.app.AlertDialog dialog, @NonNull TextView text) { + this.dialog = dialog; + this.text = text; + } + } + + private static final int VERDICT_DENIED = 0; + private static final int VERDICT_ALLOWED = 1; + private static final int VERDICT_ABNORMAL = -1; + + /** max(RAM - 8 GiB, RAM x 0.4) in bytes; -1 when meminfo is unreadable. */ + private long probeNeedBytes(long memTotalKb) { + if (memTotalKb <= 0) return -1; + long total = memTotalKb * 1024; + return Math.max(total - PROBE_KEEP_BYTES, (long) (total * PROBE_MIN_RAM_FRACTION)); + } + + private static long pagesFor(long bytes) { + return (bytes + PAGE_SIZE - 1) / PAGE_SIZE; + } + + /** Kick off the probe worker; the switch stays under the flow's control. */ + private void startCmaProbe() { + new Thread(this::runCmaProbe, "hugepage-cma-probe").start(); + } + + /** + * The consumability probe (worker thread). Steps, per the module docs: + * precondition {@code avail >= max(RAM-7G, 40% RAM)} (guided acquire, else + * save-and-reboot); then {@code echo avail > pool_want_with_cma}, + * {@code echo 0 > pool_want} (the freed blocks flip to the reservoir), run + * {@code balloon 1536} and judge from how much CmaFree the pressure consumed + * whether this vendor lets user apps allocate from CMA. {@code pool_want} is + * restored on every path. + * + *

    Only a pass is written to settings.prop ({@code cma_probe_result=1}) - + * an unreadable result asks the user, and enabling counts as a pass. A + * failure (or a decline) records nothing and clears any stale verdict, so + * the next launch can simply probe again. + */ + private void runCmaProbe() { + var cancelled = new AtomicBoolean(false); + ProbeUi ui = null; + boolean raised = false; // pool_want_with_cma raised by us + boolean zeroed = false; // pool_want emptied by us + long prevWant = -1; + try { + // Balloon pressure would squeeze (or LMK-kill) running VMs. + if (runningVmMemMib() > 0) { + probeFail(null, getString(R.string.hugepage_cma_vms_running)); + return; + } + var snap = model.state(); + prevWant = snap.targetIdeal; + long needBytes = probeNeedBytes(model.memTotalKb()); + if (needBytes <= 0) { + probeFail(null, getString(R.string.hugepage_cma_probe_failed_generic)); + return; + } + long needPages = pagesFor(needBytes); + // 1. How big a reservoir to measure against. It is NOT bounded by + // the configured pool: the probe empties the pool into the + // reservoir (pool_want=0 -> the module's shrink flips every avail + // block to CMA, instantly), so the only ceiling is the module's + // RAM-derived pool_size_max. A cap below what we want makes the + // verdict shakier, not impossible: warn and let the user go on. + long cap = model.poolSizeMax(); + long target = Math.max(needPages, Math.max(prevWant, snap.wantWithCma)); + if (cap > 0) target = Math.min(target, cap); + long goal = Math.min(needPages, target); + if (goal < needPages && !probeAsk( + getString(R.string.hugepage_cma_small_reservoir_title), + getString(R.string.hugepage_cma_small_reservoir_msg, + SizeUtils.formatSize(goal * PAGE_SIZE), + SizeUtils.formatSize(needPages * PAGE_SIZE)), + getString(R.string.hugepage_cma_probe_anyway))) { + probeCancelled(null, false); + return; + } + ui = probeProgressShow(reservoirStage(snap.cmaPool, goal), cancelled); + if (ui == null) { + probeCancelled(null, false); + return; + } + // 2. Build the reservoir. Already there (a previous run persisted the + // target and the module assembled it at boot) -> measure directly, + // touching nothing. + boolean built = snap.cmaPool >= goal; + if (!built) { + // Raise the total first: pool_want=0 would otherwise soft-disable + // the pool and hand its pages back to the buddy allocator instead + // of flipping them into the reservoir. Never lower an existing + // bigger total - that demolishes reservoir the device is lending + // out right now. + if (snap.wantWithCma < target) { + var w = model.writeWantWithCma(target); + if (!w.ok()) { + probeDismiss(ui); + probeUnavailable(getString(R.string.hugepage_cma_unavailable_write, + w.detail != null ? w.detail : "?")); + return; + } + } + raised = true; + // Empty the pool: its avail blocks flip to CMA at once (the fast + // path - a from-scratch sweep would hit the fragmentation wall). + // Only the live knob is written, so a reboot restores pool_want + // even if this app dies before the restore below. + var shrink = model.writeWant(0); + if (!shrink.ok()) { + probeRollback(); + probeDismiss(ui); + probeFail(null, getString(R.string.hugepage_cma_probe_failed_generic)); + return; + } + zeroed = true; + Thread.sleep(3000); // let the flips land + model.acquire(3); // best-effort top-up toward the target + built = waitReservoir(ui, goal, cancelled); + } + if (cancelled.get()) { + model.stopAcquire(); + probeRestore(prevWant, zeroed); + probeRollback(); + probeCancelled(ui, true); + return; + } + // Whatever actually assembled is what the balloon is judged against. + long reservoirPages = model.state().cmaPool; + if (reservoirPages <= 0) { + probeRestore(prevWant, zeroed); + probeRollback(); + probeDismiss(ui); + probeUnavailable(getString(R.string.hugepage_cma_unavailable_reservoir)); + return; + } + if (!built) { + // The runtime sweep hit the fragmentation wall. This is not a + // different failure from "it can't be built" - the module builds + // the reservoir first at init, on the cleanest memory there is, + // so the very same target simply works after a reboot. Persist + // it and pick the probe back up then (see the pending prompt) - + // or measure right now against the smaller reservoir that did + // get built, accepting a shakier verdict. + probeDismiss(ui); + ui = null; + int choice = probeAskChoice( + getString(R.string.hugepage_cma_reservoir_short_title), + getString(R.string.hugepage_cma_reservoir_short_msg, + SizeUtils.formatSize(reservoirPages * PAGE_SIZE), + SizeUtils.formatSize(goal * PAGE_SIZE)), + getString(R.string.hugepage_cma_save_reboot), + getString(R.string.hugepage_cma_probe_anyway)); + if (choice != CHOICE_NEGATIVE) { + // "I'll reboot, then probe", or dismissed. Nothing is saved: + // the probe assembles the reservoir out of the pool itself, + // so a fresh boot - where memory is unfragmented - simply + // lets the same run succeed. Undo our live writes and go. + probeRestore(prevWant, zeroed); + probeRollback(); + if (choice == CHOICE_POSITIVE) + probeToast(getString(R.string.hugepage_cma_reboot_hint)); + probeEndUi(false); + return; + } + // Probe anyway: a fresh progress dialog for the pressure stage. + ui = probeProgressShow( + getString(R.string.hugepage_cma_probe_running_balloon), cancelled); + if (ui == null) { + probeRestore(prevWant, zeroed); + probeRollback(); + probeCancelled(null, false); + return; + } + } + // 3. Pressure + judgment, measured against the reservoir that exists. + probeStage(ui, getString(R.string.hugepage_cma_probe_running_balloon)); + var out = model.runBalloon(BALLOON_FLOOR_MB, BALLOON_TIMEOUT_S); + if (cancelled.get()) { + probeRestore(prevWant, zeroed); + probeRollback(); + probeCancelled(ui, true); + return; + } + int verdict = judgeBalloon(out, reservoirPages); + probeDismiss(ui); + ui = null; + // A pass enables straight away. Anything else - a denial, or numbers + // that match neither verdict - is put to the user, because a probe + // can be wrong (a small reservoir, a vendor that only lets some + // allocation classes in). Enabling either way counts as a pass and + // is recorded, so the switch stops probing from now on; declining + // records nothing and clears any stale verdict, leaving the probe + // available next launch. + if (verdict == VERDICT_ALLOWED) { + model.setCmaProbeAllowed(); + probeEnableWithSmallPool(target); + probeToast(getString(R.string.hugepage_cma_probe_ok, + SizeUtils.formatSize(PROBE_POOL_BYTES))); + probeEndUi(true); + return; + } + boolean enable = verdict == VERDICT_DENIED + ? probeAsk(getString(R.string.hugepage_cma_probe_denied_title), + getString(R.string.hugepage_cma_probe_denied_result), + getString(R.string.hugepage_cma_probe_enable)) + : probeAskAbnormal(out, reservoirPages); + if (enable) { + model.setCmaProbeAllowed(); + probeEnableWithSmallPool(target); // also restores pool_want + probeToast(getString(R.string.hugepage_cma_enabled_pool, + SizeUtils.formatSize(PROBE_POOL_BYTES))); + probeEndUi(true); + } else { + probeRestore(prevWant, zeroed); + model.clearCmaProbeResult(); + model.saveCmaTarget(0); // demolishes the reservoir + if (zeroed) model.acquire(1); // refill the restored pool + probeEndUi(false); + } + } catch (InterruptedException e) { + probeRestore(prevWant, zeroed); + if (raised) probeRollback(); + probeCancelled(ui, ui != null); + } catch (Exception e) { + // Never leave the flow lock stuck: undo our writes and surface the + // error instead of a wedged switch. + Log.w(TAG, "CMA probe failed", e); + probeRestore(prevWant, zeroed); + if (raised) probeRollback(); + probeDismiss(ui); + probeFail(null, getString(R.string.hugepage_cma_probe_failed_generic)); + } + } + + /** Put {@code pool_want} back if the probe emptied it (live knob only). */ + private void probeRestore(long prevWant, boolean zeroed) { + if (zeroed && prevWant >= 0) model.writeWant(prevWant); + } + + /** + * The reboot half of the probe: the reservoir target survived a reboot with + * no verdict recorded, so the module has now built it on clean memory and + * the measurement can finally run. Asked once per visit; declining leaves + * the reservoir in place (it is still lent to apps) and the switch on. + */ + private void promptPendingProbe() { + if (probePromptShown || cmaBusy || isFinishing() || isDestroyed()) return; + probePromptShown = true; + new MaterialAlertDialogBuilder(this) + .setTitle(R.string.hugepage_cma_probe_pending_title) + .setMessage(R.string.hugepage_cma_probe_pending_msg) + .setPositiveButton(R.string.hugepage_cma_probe_continue, (d, w) -> { + cmaBusy = true; + startCmaProbe(); + }) + .setNegativeButton(android.R.string.cancel, null) + .show(); + } + + /** Undo the probe's only write: the raised total demolishes its reservoir. */ + private void probeRollback() { + model.writeWantWithCma(0); + } + + @NonNull + private String reservoirStage(long got, long goal) { + return getString(R.string.hugepage_cma_building, + SizeUtils.formatSize(got * PAGE_SIZE), SizeUtils.formatSize(goal * PAGE_SIZE)); + } + + /** + * The end state of an enabled probe: a {@value #PROBE_POOL_BYTES}-byte pool + * and the with-CMA total the probe assembled, persisted in one settings.prop + * rewrite so the next boot comes up the same way. This is the probe's first + * and only {@code pool_want} write: shrinking the pool hands its pages to + * the reservoir (the module's shrink path), and the v3 acquire then stages + * 512 MB back in and tops the reservoir up toward the total. + */ + private void probeEnableWithSmallPool(long total) { + model.saveTargets(PROBE_POOL_PAGES, total); + // v3: only the mode-2/3 sweep runs the reservoir-building Phase R. + model.acquire(3); + } + + /** + * Poll the reservoir toward {@code goal} pages, narrating progress. Returns + * true once {@code pool_cma} reaches it; false on stop/timeout/cancel. + */ + private boolean waitReservoir(@NonNull ProbeUi ui, long goal, + @NonNull AtomicBoolean cancelled) + throws InterruptedException { + for (int i = 0; i < 1800; i++) { // 30 min hard bound + if (cancelled.get()) return false; + var s = model.state(); + probeStage(ui, reservoirStage(s.cmaPool, goal)); + if (s.cmaPool >= goal) return true; + // Give the worker a few seconds to raise acquire_active before + // treating "not acquiring" as done-short. + if (!s.acquiring && i > 5) return false; + Thread.sleep(1000); + } + return false; + } + + /** + * Judge the balloon output against the reservoir that was built: pressure + * that consumed at least half of it means apps allocate from CMA; a tenth + * or less means they can't; anything between (or unparsable output) is + * unreadable and goes to the user. + */ + private int judgeBalloon(@Nullable Map out, long reservoirPages) { + if (out == null) return VERDICT_ABNORMAL; + long diffKb; + long heldMb; + try { + diffKb = Long.parseLong(out.getOrDefault("cma_diff_kb", "").trim()); + heldMb = Long.parseLong(out.getOrDefault("held_mb", "").trim()); + } catch (NumberFormatException e) { + return VERDICT_ABNORMAL; + } + long reservoirKb = reservoirPages * (PAGE_SIZE / 1024); + if (reservoirKb <= 0 || heldMb <= 0) return VERDICT_ABNORMAL; + if (diffKb >= reservoirKb / 2) return VERDICT_ALLOWED; + if (diffKb <= reservoirKb / 10) return VERDICT_DENIED; + return VERDICT_ABNORMAL; + } + + /* ---- probe worker <-> UI plumbing (all blocking helpers) ---- */ + + /** Blocking two-choice dialog; false on cancel/back/finish. */ + private boolean probeAsk(@NonNull String title, @NonNull String message, + @NonNull String positive) throws InterruptedException { + return probeAskChoice(title, message, positive, + getString(android.R.string.cancel)) == CHOICE_POSITIVE; + } + + /** {@link #probeAskChoice} outcomes; {@code CHOICE_NONE} = back/dismiss/gone. */ + private static final int CHOICE_NONE = 0; + private static final int CHOICE_POSITIVE = 1; + private static final int CHOICE_NEGATIVE = 2; + + /** + * Blocking dialog offering two named actions, with back/outside-tap as a + * third "neither" outcome. Both buttons are real choices - which is why + * neither is labelled Cancel by callers that need three ways out. + */ + private int probeAskChoice(@NonNull String title, @NonNull String message, + @NonNull String positive, @NonNull String negative) + throws InterruptedException { + var choice = new AtomicInteger(CHOICE_NONE); + var latch = new CountDownLatch(1); + runOnUiThread(() -> { + // isDestroyed covers rotation teardown (isFinishing stays false); + // the catch covers a window torn down mid-post - either way the + // latch MUST be counted or the worker blocks forever. + if (isFinishing() || isDestroyed()) { + latch.countDown(); + return; + } + try { + new MaterialAlertDialogBuilder(this) + .setTitle(title) + .setMessage(message) + .setPositiveButton(positive, (d, w) -> choice.set(CHOICE_POSITIVE)) + .setNegativeButton(negative, (d, w) -> choice.set(CHOICE_NEGATIVE)) + .setOnDismissListener(d -> latch.countDown()) + .show(); + } catch (Exception e) { + latch.countDown(); + } + }); + latch.await(); + return choice.get(); + } + + /** The "result unreadable - enable anyway?" dialog, with the raw numbers. */ + private boolean probeAskAbnormal(@Nullable Map out, long reservoirPages) + throws InterruptedException { + long diffKb = 0; + long heldMb = 0; + String stop = "?"; + if (out != null) { + try { + diffKb = Long.parseLong(out.getOrDefault("cma_diff_kb", "0").trim()); + } catch (NumberFormatException ignored) { + } + try { + heldMb = Long.parseLong(out.getOrDefault("held_mb", "0").trim()); + } catch (NumberFormatException ignored) { + } + stop = out.getOrDefault("stop_reason", "?"); + } + var choice = new AtomicInteger(0); + var latch = new CountDownLatch(1); + long fDiffKb = diffKb; + long fHeldMb = heldMb; + String fStop = stop; + runOnUiThread(() -> { + if (isFinishing() || isDestroyed()) { + latch.countDown(); + return; + } + try { + new MaterialAlertDialogBuilder(this) + .setTitle(R.string.hugepage_cma_probe_abnormal_title) + .setMessage(getString(R.string.hugepage_cma_probe_abnormal_msg, + SizeUtils.formatSize(fDiffKb * 1024), + SizeUtils.formatSize(reservoirPages * PAGE_SIZE), + SizeUtils.formatSize(fHeldMb * 1024 * 1024), + fStop)) + .setPositiveButton(R.string.hugepage_cma_probe_enable, + (d, w) -> choice.set(1)) + .setNegativeButton(R.string.hugepage_cma_probe_keep_off, null) + .setOnDismissListener(d -> latch.countDown()) + .show(); + } catch (Exception e) { + latch.countDown(); + } + }); + latch.await(); + return choice.get() == 1; + } + + /** Show the cancellable progress dialog; null when the activity is gone. */ + @Nullable + private ProbeUi probeProgressShow(@NonNull String initial, + @NonNull AtomicBoolean cancelled) + throws InterruptedException { + var holder = new java.util.concurrent.atomic.AtomicReference(); + var latch = new CountDownLatch(1); + runOnUiThread(() -> { + try { + if (isFinishing() || isDestroyed()) return; + float density = getResources().getDisplayMetrics().density; + var text = new TextView(this); + text.setText(initial); + var box = new LinearLayout(this); + box.setOrientation(LinearLayout.HORIZONTAL); + box.setGravity(android.view.Gravity.CENTER_VERTICAL); + int pad = Math.round(24 * density); + box.setPaddingRelative(pad, Math.round(16 * density), pad, 0); + var spinner = new ProgressBar(this); + box.addView(spinner, Math.round(32 * density), Math.round(32 * density)); + var lp = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT); + lp.setMarginStart(Math.round(16 * density)); + box.addView(text, lp); + var dialog = new MaterialAlertDialogBuilder(this) + .setTitle(R.string.hugepage_enable_cma) + .setView(box) + .setCancelable(false) + .setNegativeButton(android.R.string.cancel, null) + .create(); + dialog.show(); + // Cancel requests cooperative interruption; the worker decides + // when it is safe to stop, so the button must not dismiss the + // dialog. A running balloon ignores flags, so also kill it - + // its run() then returns quickly and the worker sees the flag. + var btn = dialog.getButton(android.content.DialogInterface.BUTTON_NEGATIVE); + if (btn != null) btn.setOnClickListener(v -> { + cancelled.set(true); + v.setEnabled(false); + runOnPool(() -> runList("pkill", "-f", + "gh-hugepage-reserve/balloon")); + }); + holder.set(new ProbeUi(dialog, text)); + } catch (Exception ignored) { + // window torn down mid-post: holder stays null = "activity gone" + } finally { + latch.countDown(); + } + }); + latch.await(); + return holder.get(); + } + + private void probeStage(@NonNull ProbeUi ui, @NonNull String msg) { + runOnUiThread(() -> ui.text.setText(msg)); + } + + private void probeDismiss(@Nullable ProbeUi ui) { + if (ui != null) runOnUiThread(ui.dialog::dismiss); + } + + private void probeToast(@NonNull String msg) { + runOnUiThread(() -> Toast.makeText(this, msg, Toast.LENGTH_LONG).show()); + } + + /** Wind the flow down: dismiss, switch off. Callers own the rollback. */ + private void probeCancelled(@Nullable ProbeUi ui, boolean toast) { + probeDismiss(ui); + if (toast) probeToast(getString(R.string.hugepage_cma_probe_cancelled)); + probeEndUi(false); + } + + /** Failure with a message dialog (or toast when {@code title} is null). */ + private void probeFail(@Nullable String title, @NonNull String message) { + runOnUiThread(() -> { + cmaBusy = false; + setCmaSwitch(false); + if (isFinishing()) return; + if (title == null) { + Toast.makeText(this, message, Toast.LENGTH_LONG).show(); + } else { + new MaterialAlertDialogBuilder(this) + .setTitle(title) + .setMessage(message) + .setPositiveButton(android.R.string.ok, null) + .show(); + } + refreshStatus(); + }); + } + + private void probeUnavailable(@NonNull String detail) { + probeFail(getString(R.string.hugepage_cma_unavailable_title), detail); + } + + /** Release the flow lock and settle the switch to the final state. */ + private void probeEndUi(boolean enabled) { + runOnUiThread(() -> { + cmaBusy = false; + setCmaSwitch(enabled); + cmaInputLoaded = false; // reseed the with-CMA total field + loadPoolSize(); // a passing probe pins pool_want to 512 MB + refreshStatus(); + }); + } + private void confirmUnload() { new MaterialAlertDialogBuilder(this) .setTitle(R.string.hugepage_stop) diff --git a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageColor.java b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageColor.java index 19f71c334..7287fbef9 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageColor.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageColor.java @@ -7,15 +7,32 @@ import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.TreeSet; + import cn.classfun.droidvm.R; /** * Deterministic per-process colors for the hugepage usage bar and list icons. - * The same pid always maps to the same hue (stable across refreshes); in dark - * mode colors are light, in light mode they are dark, so they read against the - * surface either way. + * In dark mode colors are light, in light mode they are dark, so they read + * against the surface either way. + * + *

    Hues are assigned by golden-angle steps over each pid's rank among the + * pids currently shown (ascending pid order), not over the raw pid. Keying + * the angle by pid could land two live VMs almost on the same hue (any pid + * difference near a multiple of 360/137.508 - e.g. 34 - wraps to within a few + * degrees), whereas consecutive ranks are always 137.5° apart, so no two of + * up to ~7 VMs come closer than ~32° (≥52° for four or fewer). Ranks + * are stable while the same VMs run - new pids are usually larger and append at + * the end - and both hugepage screens derive the map from the same owner list, + * so a VM keeps one color everywhere until the set of VMs itself changes. */ final class HugePageColor { + /** Golden angle in degrees: consecutive ranks land maximally apart. */ + private static final float GOLDEN_ANGLE = 137.508f; + private HugePageColor() { } @@ -25,9 +42,22 @@ static boolean isDark(Context ctx) { return mode == Configuration.UI_MODE_NIGHT_YES; } - /** Golden-angle hue spread keyed by pid, theme-aware saturation/value. */ - static int forPid(int pid, boolean dark) { - float hue = ((pid * 137.508f) % 360f + 360f) % 360f; + /** + * Assign a color to every pid in {@code pids} (duplicates collapse), keyed + * by ascending-pid rank. Pass the full pid list of the screen (not a + * filtered subset) so both hugepage screens agree on the ranks. + */ + @NonNull + static Map forPids(@NonNull Collection pids, boolean dark) { + var map = new LinkedHashMap(); + int rank = 0; + for (var pid : new TreeSet<>(pids)) map.put(pid, forRank(rank++, dark)); + return map; + } + + /** Golden-angle hue for one rank, theme-aware saturation/value. */ + static int forRank(int rank, boolean dark) { + float hue = ((rank * GOLDEN_ANGLE) % 360f + 360f) % 360f; float[] hsv = {hue, dark ? 0.50f : 0.72f, dark ? 0.90f : 0.55f}; return Color.HSVToColor(hsv); } @@ -36,4 +66,27 @@ static int forPid(int pid, boolean dark) { static int pending(@NonNull Context ctx) { return ContextCompat.getColor(ctx, R.color.hugepage_pending); } + + /** Reservoir portion currently occupied by other apps' allocations. */ + static int cmaUsed(@NonNull Context ctx) { + return ContextCompat.getColor(ctx, R.color.hugepage_cma_used); + } + + /** Reservoir portion still free in buddy. */ + static int cmaFree(@NonNull Context ctx) { + return ContextCompat.getColor(ctx, R.color.hugepage_cma_free); + } + + /** Available-pool portion not flippable to CMA as whole pageblocks. */ + static int availNonCma(@NonNull Context ctx) { + return ContextCompat.getColor(ctx, R.color.hugepage_avail_non_cma); + } + + /** + * Opaque gray for the synthetic "available" list row's icon - the bar's + * translucent track color is too faint as an icon tint. + */ + static int availIcon(boolean dark) { + return dark ? 0xFFB5B5B5 : 0xFF8A8A8A; + } } diff --git a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageModel.java b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageModel.java index 30b7fb4b8..350283e91 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageModel.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageModel.java @@ -6,6 +6,7 @@ import static cn.classfun.droidvm.lib.utils.RunUtils.run; import static cn.classfun.droidvm.lib.utils.RunUtils.runList; import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; +import static cn.classfun.droidvm.lib.utils.StringUtils.joinNonEmpty; import static cn.classfun.droidvm.lib.utils.StringUtils.pathJoin; import android.os.SystemClock; @@ -21,6 +22,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.LongSupplier; import java.util.function.Supplier; import java.util.regex.Pattern; @@ -56,6 +58,12 @@ final class HugePageModel { private static final String DISABLE_FILE = pathJoin(MAGISK_BASE, "disable"); private static final String SETTINGS_PROP = pathJoin(MAGISK_BASE, "settings.prop"); private static final String KO_PATH = pathJoin(MAGISK_BASE, "gh_hugepage_reserve.ko"); + /** The module's own preflight+insmod script (v10.1+); preferred load path. */ + private static final String LOAD_SCRIPT = pathJoin(MAGISK_BASE, "load.sh"); + /** ABI/BTF preflight helper the boot script feeds into insmod. */ + private static final String KAPI_CHECK = pathJoin(MAGISK_BASE, "kapi_check"); + /** insmod params are pasted into a shell line: allow only inert characters. */ + private static final Pattern SAFE_PARAM = Pattern.compile("[A-Za-z0-9_,.-]+"); /** A THP huge page is 2 MiB = 2048 KiB. */ private static final long KB_PER_PAGE = 2048; /** Default target if settings.prop has none (pages): 1024 x 2 MB = 2 GB. */ @@ -159,55 +167,117 @@ static final class UsageEntry { /** * Version-unified snapshot of module + pool state. {@code targetIdeal} is the * one "target" concept across versions: v7's {@code pool_want}, else v6's - * read-only {@code pool_target}, else current capacity. {@code deficit} is what - * is still missing toward it ({@code targetIdeal - free - lent}); the acquire - * buttons are usable exactly when {@code loaded && deficit > 0}. + * read-only {@code pool_target}, else current capacity. {@code deficit} is the + * work acquire still has - the larger of the pool shortfall and the reservoir + * shortfall (see {@link #state()}); the acquire buttons are usable exactly when + * {@code loaded && deficit > 0}. It is not the bar's waiting-to-acquire + * block, which each screen derives from its own segments. */ static final class Snapshot { - final boolean installed; // Magisk module files present - final boolean loaded; // insmod'd (sysfs node exists) - final boolean statsOk; // refill_stat was readable (false on a transient failure) - final boolean bootEnabled; // will load next boot (no Magisk disable file) - final long targetIdeal; // unified target (want): pool_want ?? pool_target ?? built - final long built; // pool_total (capacity assembled) - final long free; // pool_avail (in the pool now) - final long lent; // served (out to VMs); 0 if the module can't report it (v6) - final long deficit; // max(0, targetIdeal - free - lent) - final boolean acquiring; // an acquire worker is running - final int acquireMode; // which mode (1/2/3), or -1 if the module can't report it - final boolean hasPoolWant; // pool_want knob reported (v7 runtime-resizable) - final boolean softDisabled; // v7 pool_want <= 1 (reserve released, module stays loaded) + // Field defaults double as the not-loaded snapshot (the short ctor + // touches only installed/bootEnabled and leaves the rest as declared). + boolean installed = false; // Magisk module files present + boolean loaded = false; // insmod'd (sysfs node exists) + boolean statsOk = false; // refill_stat was readable (false on a transient failure) + boolean bootEnabled = false; // will load next boot (no Magisk disable file) + long targetIdeal = 0; // unified target (want): pool_want ?? pool_target ?? built + long built = 0; // pool_total (capacity assembled) + long free = 0; // pool_avail (in the pool now) + long lent = 0; // served (out to VMs); 0 if the module can't report it (v6) + long deficit = 0; // toward want-with-cma (v10 on) or targetIdeal + boolean acquiring = false; // an acquire worker is running + int acquireMode = -1; // which mode (1/2/3), or -1 if the module can't report it + boolean hasPoolWant = false; // pool_want knob reported (v7 runtime-resizable) + boolean softDisabled = false; // v7 pool_want <= 1 (reserve released, module stays loaded) + // v10 CMA reservoir (refill_stat additions); all -1 / 0 on pre-v10 modules. + boolean hasCma = false; // refill_stat reports pool_want_with_cma (v10 module) + long wantWithCma = 0; // total target incl. reservoir (pages); 0 = CMA off + long cmaPool = 0; // reservoir size (2 MB-page equivalents) + long availCmaAble = -1; // avail pages flippable as whole pageblocks; -1 unreported + int cmaPbOrder = -1; // pageblock order; -1 = CMA side off this boot // Raw display strings from refill_stat (pass-through, "-" when absent). - @NonNull final String state; - @NonNull final String totalServed; - @NonNull final String totalRefilled; - @NonNull final String activeVms; - @NonNull final String acquireStopReason; // why the last acquire stopped ("-" if unreported) - - private Snapshot(boolean installed, boolean loaded, boolean statsOk, boolean bootEnabled, - long targetIdeal, long built, long free, long lent, long deficit, - boolean acquiring, int acquireMode, boolean hasPoolWant, - boolean softDisabled, @NonNull String state, @NonNull String totalServed, - @NonNull String totalRefilled, @NonNull String activeVms, - @NonNull String acquireStopReason) { + @NonNull String state = "-"; + @NonNull String totalServed = "-"; + @NonNull String totalRefilled = "-"; + @NonNull String activeVms = "-"; + @NonNull String acquireStopReason = "-"; // why the last acquire stopped ("-" if unreported) + + /** + * Not-loaded snapshot: only the install + next-boot flags are known; + * every pool field keeps its default (the {@code loaded == false} view). + */ + Snapshot(boolean installed, boolean bootEnabled) { this.installed = installed; - this.loaded = loaded; - this.statsOk = statsOk; this.bootEnabled = bootEnabled; - this.targetIdeal = targetIdeal; - this.built = built; - this.free = free; - this.lent = lent; - this.deficit = deficit; - this.acquiring = acquiring; - this.acquireMode = acquireMode; - this.hasPoolWant = hasPoolWant; - this.softDisabled = softDisabled; - this.state = state; - this.totalServed = totalServed; - this.totalRefilled = totalRefilled; - this.activeVms = activeVms; - this.acquireStopReason = acquireStopReason; + } + + /** + * Loaded snapshot parsed straight from a {@code refill_stat} key=value + * map. {@code poolTargetFn} supplies v6's read-only {@code pool_target} + * lazily - it is only consulted when {@code pool_want} is absent. + */ + Snapshot(@NonNull Map s, boolean installed, boolean bootEnabled, + @NonNull LongSupplier poolTargetFn) { + this.installed = installed; + this.bootEnabled = bootEnabled; + this.loaded = true; + this.statsOk = !s.isEmpty(); // empty == the read failed transiently + this.hasPoolWant = s.containsKey("pool_want"); + this.built = getLong(s, "pool_total", 0); + this.free = getLong(s, "pool_avail", 0); + this.lent = getLong(s, "served", 0); + long rawWant = getLong(s, "pool_want", -1); + long want = rawWant; + if (want < 0) want = poolTargetFn.getAsLong(); // v6: separate read-only knob + if (want < 0) want = built; // last resort: current capacity + this.targetIdeal = want; + // v10 reservoir state; pre-v10 modules report none of these keys. + this.hasCma = s.containsKey("pool_want_with_cma"); + this.wantWithCma = getLong(s, "pool_want_with_cma", 0); + this.cmaPool = getLong(s, "pool_cma", 0); + this.availCmaAble = getLong(s, "pool_avail_cma_able", -1); + this.cmaPbOrder = (int) getLong(s, "cma_pb_order", -1); + // What acquire still has to do, mirroring the module's own "already at + // target" test (acquire_set): it runs when the POOL is short + // (avail + served < pool_want) OR the RESERVOIR is short + // (avail + served + pool_cma < pool_want_with_cma). Those are separate + // shortfalls: raising pool_want while the reservoir already covers the + // total leaves no total deficit, yet acquire must still stage pages in + // from the reservoir to fill the pool. Reporting only the total would + // grey out the acquire buttons exactly then. + long poolDeficit = Math.max(0, want - free - lent); + long reservoirDeficit = wantWithCma > 0 + ? Math.max(0, wantWithCma - free - lent - cmaPool) : 0; + this.deficit = Math.max(poolDeficit, reservoirDeficit); + this.acquiring = "1".equals(s.get("acquire_active")); + this.acquireMode = (int) getLong(s, "acquire_mode", -1); + this.softDisabled = hasPoolWant && rawWant <= 1; + this.state = s.getOrDefault("state", "-"); + this.totalServed = s.getOrDefault("total_served", "-"); + this.totalRefilled = s.getOrDefault("total_refilled", "-"); + this.activeVms = s.getOrDefault("active_vms", "-"); + this.acquireStopReason = s.getOrDefault("acquire_stop_reason", "-"); + } + + /** The v10 reservoir is on right now (module tracks a with-CMA total). */ + boolean cmaActive() { + return hasCma && wantWithCma > 0; + } + } + + /** + * Reservoir occupancy snapshot from the {@code cma_usage} knob (~1 s cache in + * the module). {@code ok == false} when the knob is absent/unreadable - the + * caller then shows the reservoir as one undivided block. Only the occupied + * amount is carried: the free part is derived from {@code pool_cma}. + */ + static final class CmaUsage { + final boolean ok; + final long usedMb; + + private CmaUsage(boolean ok, long usedMb) { + this.ok = ok; + this.usedMb = usedMb; } } @@ -224,29 +294,25 @@ Snapshot state() { boolean installed = existsSticky(MODULE_PROP); boolean bootEnabled = !shellCheckExists(DISABLE_FILE); boolean loaded = existsSticky(SYSFS_BASE); - if (!loaded) { - return new Snapshot(installed, false, false, bootEnabled, - 0, 0, 0, 0, 0, false, -1, false, false, "-", "-", "-", "-", "-"); - } + if (!loaded) return new Snapshot(installed, bootEnabled); var s = parseProp(safeRead(pathJoin(SYSFS_PARAMS, "refill_stat"))); - boolean statsOk = !s.isEmpty(); // empty == the read failed transiently - boolean hasWant = s.containsKey("pool_want"); - long built = getLong(s, "pool_total", 0); - long free = getLong(s, "pool_avail", 0); - long lent = getLong(s, "served", 0); - long rawWant = getLong(s, "pool_want", -1); - long want = rawWant; - if (want < 0) want = poolTarget(); // v6: separate read-only knob - if (want < 0) want = built; // last resort: current capacity - long deficit = Math.max(0, want - free - lent); - boolean acquiring = "1".equals(s.get("acquire_active")); - int mode = (int) getLong(s, "acquire_mode", -1); - boolean softDisabled = hasWant && rawWant <= 1; - return new Snapshot(installed, true, statsOk, bootEnabled, - want, built, free, lent, deficit, acquiring, mode, hasWant, softDisabled, - s.getOrDefault("state", "-"), s.getOrDefault("total_served", "-"), - s.getOrDefault("total_refilled", "-"), s.getOrDefault("active_vms", "-"), - s.getOrDefault("acquire_stop_reason", "-")); + return new Snapshot(s, installed, bootEnabled, this::poolTarget); + } + + /** Reservoir occupancy from {@code cma_usage}; {@code ok=false} when absent. */ + @NonNull + CmaUsage cmaUsage() { + var raw = safeRead(pathJoin(SYSFS_PARAMS, "cma_usage")); + if (raw.trim().isEmpty()) return new CmaUsage(false, 0); + // Tokens are key=value but not strictly one per line (blocks_* share a + // line), so scan word-wise instead of reusing the line parser. + var map = new LinkedHashMap(); + for (var tok : raw.split("[\\s\\n]+")) { + var parts = tok.split("=", 2); + if (parts.length == 2) map.put(parts[0].trim(), parts[1].trim()); + } + if (!map.containsKey("reservoir_mb")) return new CmaUsage(false, 0); + return new CmaUsage(true, getLong(map, "used_mb", 0)); } /** @@ -278,26 +344,6 @@ boolean koAvailable() { return existsSticky(pathJoin(SYSFS_PARAMS, "served_summary")); } - /** - * Save a new pool target of {@code pages}. Persists it to settings.prop (for - * the next boot) and applies it to the running pool if the live {@code pool_want} - * knob exists; on v6 (read-only {@code pool_target}) only the persist takes - * effect. Does not fire an acquire -- the caller drives that from the - * resulting {@link Snapshot#deficit}. - */ - @NonNull - Result saveSize(long pages) { - var persisted = writeSettings(pages); - if (!persisted.ok()) return Result.failed("settings", persisted.error); - // Best-effort live apply; the impl reports whether it actually took effect - // on the running pool ("pool_want") or only persisted for next boot - // ("settings" - v6's read-only target, or a rejected write). - var live = writeKnob("pool_want", Long.toString(pages)); - // Degraded when the live write didn't land (v6 read-only target, or a - // rejected write): only the next-boot persist took effect. - return Result.ok(live.ok() ? "pool_want" : "settings", !live.ok()); - } - /** * Bring the running pool up (true) or down (false), choosing the deepest action * the module supports: @@ -393,12 +439,211 @@ Result stopAcquire() { return t.ok() ? Result.ok("acquire") : Result.unsupported(t.error); } + /* ================================================================== */ + /* v10 CMA reservoir + consumability probe */ + /* ================================================================== */ + + /** + * The app-side probe verdict recorded in settings.prop as + * {@code cma_probe_result}: {@code 1} = apps can consume the reservoir, + * {@code 0} = they can't (the boot script then keeps the whole CMA side + * cold), {@code null} = the probe never ran. + */ + @Nullable + Integer cmaProbeResult() { + var v = parseProp(safeRead(SETTINGS_PROP)).get("cma_probe_result"); + if (v == null) return null; + try { + return Integer.parseInt(v.trim()); + } catch (NumberFormatException e) { + return null; + } + } + + /** + * Record a passed probe (see {@link #cmaProbeResult}). Only success + * is ever persisted: a failed or inconclusive probe leaves no verdict, so + * the next app launch can simply probe again. + */ + @NonNull + Result setCmaProbeAllowed() { + var changes = new LinkedHashMap(); + changes.put("cma_probe_result", "1"); + var t = updateSettings(changes); + return t.ok() ? Result.ok("settings") : Result.failed("settings", t.error); + } + + /** + * Drop any recorded verdict. Beyond "forget a failure", this un-sticks a + * legacy {@code cma_probe_result=0}: the boot script hands the module -1 + * preflight values while that key is 0, which kills the CMA side for the + * whole boot and makes a re-probe impossible until it is gone. + */ + @NonNull + Result clearCmaProbeResult() { + var changes = new LinkedHashMap(); + changes.put("cma_probe_result", null); // null value = remove the key + var t = updateSettings(changes); + return t.ok() ? Result.ok("settings") : Result.failed("settings", t.error); + } + + /** + * The last non-zero with-CMA total (pages) the user ran with, kept under an + * app-owned settings.prop key so switching CMA off (which must persist + * {@code pool_want_with_cma=0} for the boot script) doesn't forget the size. + */ + long lastCmaTargetPages() { + var s = parseProp(safeRead(SETTINGS_PROP)); + var v = s.get("pool_want_with_cma_last"); + if (v != null) try { + return Long.parseLong(v.trim()); + } catch (NumberFormatException ignored) { + } + return 0; + } + + /** + * Persist the with-CMA total for the next boot and best-effort apply it to + * the running module. A non-zero target is also remembered under the + * {@code _last} key for the next re-enable. Like {@link #saveTargets}, the + * result reports whether the live write landed ({@code degraded} when only + * the persist took effect). + */ + @NonNull + Result saveCmaTarget(long pages) { + var persisted = updateSettings(cmaTargetChanges(pages)); + if (!persisted.ok()) return Result.failed("settings", persisted.error); + var live = writeKnob("pool_want_with_cma", Long.toString(pages)); + return Result.ok(live.ok() ? "pool_want_with_cma" : "settings", !live.ok()); + } + + /** + * Persist pool target and with-CMA total in ONE settings.prop rewrite, then + * apply both live knobs ({@code pool_want} first - a value above the old + * with-CMA total drags it up, and the second write then pins it exact). + * {@code withCma < 0} leaves the CMA keys untouched and only sets the pool + * target (persisted under both {@code pool_want} and legacy + * {@code pool_target}; on v6 only the persist takes effect). Does not + * fire an acquire -- the caller drives that from {@link Snapshot#deficit}. + */ + @NonNull + Result saveTargets(long pages, long withCma) { + // Invariant (module plan.md sec.1): pool_want <= pool_want_with_cma. The kernel + // clamps a low live write itself, but the persisted pair must agree + // too or the next boot would insmod inconsistent targets. + if (withCma >= 0) withCma = Math.max(withCma, pages); + var changes = new LinkedHashMap(); + changes.put("pool_want", Long.toString(pages)); + changes.put("pool_target", Long.toString(pages)); + if (withCma >= 0) changes.putAll(cmaTargetChanges(withCma)); + var persisted = updateSettings(changes); + if (!persisted.ok()) return Result.failed("settings", persisted.error); + var live = writeKnob("pool_want", Long.toString(pages)); + boolean liveOk = live.ok(); + if (withCma >= 0) + liveOk &= writeKnob("pool_want_with_cma", Long.toString(withCma)).ok(); + return Result.ok(liveOk ? "pool_want" : "settings", !liveOk); + } + + @NonNull + private static Map cmaTargetChanges(long pages) { + var changes = new LinkedHashMap(); + changes.put("pool_want_with_cma", Long.toString(pages)); + if (pages > 0) changes.put("pool_want_with_cma_last", Long.toString(pages)); + return changes; + } + + /** Live {@code pool_want_with_cma} write only - the probe's first step. */ + @NonNull + Result writeWantWithCma(long pages) { + var t = writeKnob("pool_want_with_cma", Long.toString(pages)); + return t.ok() ? Result.ok("pool_want_with_cma") + : Result.failed("pool_want_with_cma", t.error); + } + + /** + * Live {@code pool_want} write only (no settings.prop persist), so a value + * written here is undone by a reboot. The probe uses it to empty the pool + * into the reservoir and to put it back afterwards. + */ + @NonNull + Result writeWant(long pages) { + var t = writeKnob("pool_want", Long.toString(pages)); + return t.ok() ? Result.ok("pool_want") : Result.failed("pool_want", t.error); + } + + /** MemTotal from /proc/meminfo in KiB, or -1 if unreadable. */ + long memTotalKb() { + return meminfoKb("MemTotal"); + } + + /** + * The module's RAM-derived cap on the targets (pages), read-only: + * {@code min(ram - min(ram/2, 6G), 24G)}. Both {@code pool_want} and + * {@code pool_want_with_cma} are clamped to it, so it bounds how much + * reservoir can sit on top of a given pool. -1 when unreadable (pre-v7). + */ + long poolSizeMax() { + try { + var v = shellReadFile(pathJoin(SYSFS_PARAMS, "pool_size_max")).trim(); + if (!v.isEmpty()) return Long.parseLong(v); + } catch (Exception ignored) { + } + return -1; + } + + private long meminfoKb(@NonNull String key) { + var prefix = fmt("%s:", key); + for (var line : safeRead("/proc/meminfo").split("\n")) { + if (!line.startsWith(prefix)) continue; + var digits = NON_DIGITS.matcher(line).replaceAll(""); + if (!digits.isEmpty()) try { + return Long.parseLong(digits); + } catch (NumberFormatException ignored) { + } + } + return -1; + } + + /** + * Run the module-shipped {@code balloon} pressure tool (see tools/balloon.c): + * it anon-balloons until MemAvailable drops under {@code floorMb} and prints + * {@code cma_before_kb / cma_after_kb / cma_diff_kb / held_mb / stop_reason}. + * Supervised with {@code timeout} (falling back to unsupervised where the + * command is missing) because on a kernel that does NOT let anon consume CMA + * the floor can fire very late or never. Returns the parsed key=value output, + * or {@code null} when the tool is absent, was killed, or printed nothing + * usable - the caller treats that as an unreadable verdict. + */ + @Nullable + Map runBalloon(long floorMb, long timeoutSec) { + var bin = pathJoin(MAGISK_BASE, "balloon"); + if (!existsSticky(bin)) return null; + var r = run("chmod 755 %s && timeout %d %s %d", + escapedString(bin), timeoutSec, escapedString(bin), floorMb); + if (r.getCode() == 127) // no timeout applet on this ROM + r = run("%s %d", escapedString(bin), floorMb); + if (!r.isSuccess()) return null; + var map = parseProp(r.getOutString()); + return map.containsKey("cma_diff_kb") ? map : null; + } + /* ================================================================== */ /* Ladder plumbing (internal) */ /* ================================================================== */ - /** insmod ladder: both keys, then {@code pool_want=} (v7), {@code pool_target=} (v6), bare. */ - private enum LoadImpl { BOTH, POOL_WANT, POOL_TARGET, BARE } + /** + * insmod ladder, richest first. {@code load.sh} is the module's own + * preflight+insmod (v10+) and needs nothing from us. The remaining rungs + * exist only for the released modules that predate it: v9 wants its ABI + * guard, v6..v8 take size keys alone. Each rung drops the parameter group an + * older module wouldn't recognise. + */ + private enum LoadImpl { + SCRIPT, + GUARD_BOTH, GUARD_WANT, GUARD_TARGET, // ABI guard (v9) + BOTH, POOL_WANT, POOL_TARGET, BARE // no guard (v6/v7/v8) + } /** Marker for single-implementation actions. */ private enum Only { DEFAULT } @@ -440,8 +685,32 @@ private static Usage usageOf(@NonNull List>> log) { @NonNull private List> loadLadder(long pages) { var log = new ArrayList>(); + // Best rung: the module's own load.sh - the exact preflight+insmod the + // boot script performs, so a runtime enable reproduces the boot-time + // configuration and future preflight changes need no app change. It + // reads the size from settings.prop, which is where `pages` came from. + if (existsSticky(LOAD_SCRIPT) && rung(null, log, LoadImpl.SCRIPT, () -> { + var r = run("sh %s", escapedString(LOAD_SCRIPT)); + return (r.isSuccess() && existsSticky(SYSFS_BASE)) + ? Try.ok(LoadImpl.SCRIPT, null) + : Try.fail(LoadImpl.SCRIPT, reason(r, "load.sh")); + })) return log; var w = fmt("pool_want=\"%d\"", pages); var t = fmt("pool_target=\"%d\"", pages); + // No load.sh: a released module that predates it (v6..v9). Those have no + // CMA parameters at all, so there is nothing to reconstruct here - only + // v9's ABI guard, without which an ABI-drifted symbol can kCFI-panic on + // first call. Every v10+ preflight lives in load.sh alone, so it can + // never drift from what this app passes. + var guard = kapiGuardArg(); + if (!guard.isEmpty()) { + if (rung(null, log, LoadImpl.GUARD_BOTH, + () -> insmod(LoadImpl.GUARD_BOTH, joinNonEmpty(" ", guard, w, t)))) return log; + if (rung(null, log, LoadImpl.GUARD_WANT, + () -> insmod(LoadImpl.GUARD_WANT, joinNonEmpty(" ", guard, w)))) return log; + if (rung(null, log, LoadImpl.GUARD_TARGET, + () -> insmod(LoadImpl.GUARD_TARGET, joinNonEmpty(" ", guard, t)))) return log; + } // Pass BOTH size params first. A lenient kernel silently ignores the param // the module doesn't have, so it still gets the right size via the one it // does - pool_want (v7) or pool_target (v6). This is essential: on a v6 @@ -450,7 +719,7 @@ private List> loadLadder(long pages) { // Strict kernels reject the unknown param, so fall back to each key alone, // then a bare (default-size) load. if (rung(null, log, LoadImpl.BOTH, - () -> insmod(LoadImpl.BOTH, fmt("%s %s", w, t)))) return log; + () -> insmod(LoadImpl.BOTH, joinNonEmpty(" ", w, t)))) return log; if (rung(null, log, LoadImpl.POOL_WANT, () -> insmod(LoadImpl.POOL_WANT, w))) return log; if (rung(null, log, LoadImpl.POOL_TARGET, @@ -459,6 +728,29 @@ private List> loadLadder(long pages) { return log; } + /** + * v9's ABI guard, read from the module's {@code kapi_check} helper: it + * compares the running kernel's real symbol signatures (from vmlinux BTF) + * against what this .ko expects and names the drifted ones, which insmod + * then leaves unresolved (their feature returns -ENOSYS) instead of + * kCFI-panicking on first call. "" when the helper is absent (v6..v8) or + * nothing drifted - fail-open, exactly like the v9 boot script. + */ + @NonNull + private String kapiGuardArg() { + if (!existsSticky(KAPI_CHECK)) return ""; + var out = run("%s /sys/kernel/btf/vmlinux", escapedString(KAPI_CHECK)); + for (var line : out.getOutString().split("\n")) { + line = line.trim(); + if (!line.startsWith("disable=")) continue; + var v = line.substring("disable=".length()).trim(); + // A symbol-name list, pasted into a shell line: refuse anything else. + if (!v.isEmpty() && SAFE_PARAM.matcher(v).matches()) + return fmt("disable_kapi=%s", v); + } + return ""; + } + private static Try insmod(@NonNull LoadImpl impl, @Nullable String arg) { var r = (arg == null) ? run("insmod %s", escapedString(KO_PATH)) @@ -512,11 +804,52 @@ private static Try writeKnobTry( /** Persist the target to settings.prop under both keys (whichever loader reads). */ private static Try writeSettings(long pages) { - var a = run("echo 'pool_want=%s' > %s", pages, SETTINGS_PROP); - var b = run("echo 'pool_target=%s' >> %s", pages, SETTINGS_PROP); - return (a.isSuccess() && b.isSuccess()) - ? Try.ok(Only.DEFAULT, null) - : Try.fail(Only.DEFAULT, reason(a.isSuccess() ? b : a, "settings.prop")); + var changes = new LinkedHashMap(); + changes.put("pool_want", Long.toString(pages)); + changes.put("pool_target", Long.toString(pages)); + return updateSettings(changes); + } + + /** Serializes settings.prop read-modify-write cycles within this process. */ + private static final Object SETTINGS_LOCK = new Object(); + + /** + * Read-modify-write settings.prop: apply {@code changes} (a null value + * removes that key) and keep every other key (the file also carries + * app-owned CMA state - {@code pool_want_with_cma}, {@code cma_probe_result} + * - that a blind rewrite would wipe). The boot script {@code source}s the + * file, so lines stay plain {@code key=value}. Locked so concurrent writers + * (the probe worker vs a pool-size save) can't interleave their read/write + * pairs and drop each other's keys. + */ + private static Try updateSettings(@NonNull Map changes) { + synchronized (SETTINGS_LOCK) { + String raw; + try { + raw = shellReadFile(SETTINGS_PROP); + } catch (Exception e) { + // A missing file legitimately starts empty; an EXISTING file that + // failed to read must abort - rewriting from an empty map would + // silently drop every other persisted key (probe verdict, CMA + // targets) on a transient root hiccup. + if (existsSticky(SETTINGS_PROP)) + return Try.fail(Only.DEFAULT, "settings.prop: read failed"); + raw = ""; + } + var s = parseProp(raw); + for (var e : changes.entrySet()) { + if (e.getValue() == null) s.remove(e.getKey()); + else s.put(e.getKey(), e.getValue()); + } + var content = new StringBuilder(); + for (var e : s.entrySet()) + content.append(e.getKey()).append('=').append(e.getValue()).append('\n'); + var r = run("printf '%%s' %s > %s", + escapedString(content.toString()), SETTINGS_PROP); + return r.isSuccess() + ? Try.ok(Only.DEFAULT, null) + : Try.fail(Only.DEFAULT, reason(r, "settings.prop")); + } } @NonNull diff --git a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcess.java b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcess.java index c8ebec89f..406faec50 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcess.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcess.java @@ -35,13 +35,36 @@ public final class HugePageProcess { public final int color; /** True for the synthetic "unattributed" pool usage row (no actions). */ public final boolean unknown; - /** True for the synthetic "waiting for acquire" deficit row (acquire btn). */ + /** True for the synthetic "waiting for acquire" deficit row. */ public final boolean acquire; + /** + * Free-text detail line for synthetic rows (e.g. the available row's + * cma-able/non-cma-able breakdown); null hides the line. + */ + @Nullable + public final String detail; + /** + * Draw the three acquire buttons on this row. Normally the deficit row owns + * them, but that row is hidden once nothing is waiting to be acquired - and + * acquire can still have work (staging reservoir pages into a grown pool), + * so the CMA row picks them up then. + */ + public final boolean acquireSlots; public HugePageProcess( int pid, @NonNull String comm, long servedPages, long thpKb, char state, @Nullable String vmName, boolean alive, int color, boolean unknown, boolean acquire + ) { + this(pid, comm, servedPages, thpKb, state, vmName, alive, + color, unknown, acquire, null, acquire); + } + + public HugePageProcess( + int pid, @NonNull String comm, long servedPages, + long thpKb, char state, @Nullable String vmName, boolean alive, + int color, boolean unknown, boolean acquire, @Nullable String detail, + boolean acquireSlots ) { this.pid = pid; this.comm = comm; @@ -53,6 +76,8 @@ public HugePageProcess( this.color = color; this.unknown = unknown; this.acquire = acquire; + this.detail = detail; + this.acquireSlots = acquireSlots; } /** THP occupancy expressed in 2MiB pages (-1 if unknown). */ diff --git a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcessActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcessActivity.java index 20a47990e..53f4e9809 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcessActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcessActivity.java @@ -51,6 +51,10 @@ public final class HugePageProcessActivity extends AppCompatActivity private static final long ACQUIRE_POLL_MS = 500; private static final int ACQUIRE_POLL_MAX = 1200; // ~10 min ceiling private static final long HUGE_PAGE_BYTES = 2L * 1024 * 1024; + // Fake pids for the synthetic list rows (adapter uses pid as a stable id; + // pid 0 already belongs to the "waiting for acquire" row). + private static final int PID_AVAIL = -2; + private static final int PID_CMA = -3; private final Handler handler = new Handler(Looper.getMainLooper()); private final HugePageModel model = new HugePageModel(); @@ -218,7 +222,49 @@ private void refresh() { int kernelMode = snap.acquireMode; long holdKb = snap.loaded ? snap.free * 2048 : 0; // avail long wantKb = snap.loaded ? snap.targetIdeal * 2048 : tracedKb; // want - long deficitKb = wantKb - holdKb - tracedKb; + // v10 reservoir: mirror the status screen - the bar's denominator + // becomes pool_want_with_cma and the reservoir counts as filled. + boolean cmaOn = snap.loaded && snap.cmaActive(); + var cmaUsage = cmaOn ? model.cmaUsage() : null; + long cmaKb = cmaOn ? snap.cmaPool * 2048 : 0; + long cmaOtherKb = (cmaUsage != null && cmaUsage.ok) + ? Math.min(cmaKb, cmaUsage.usedMb * 1024) : 0; + long cmaFreeKb = cmaKb - cmaOtherKb; + long availCmaAbleKb = (cmaOn && snap.availCmaAble >= 0) + ? Math.min(holdKb, snap.availCmaAble * 2048) : -1; + long availNonCmaKb = availCmaAbleKb >= 0 ? holdKb - availCmaAbleKb : 0; + long barWantKb = cmaOn ? snap.wantWithCma * 2048 : wantKb; + long deficitKb = barWantKb - holdKb - tracedKb - cmaKb; + // Synthetic rows mirroring the bar blocks: available (with its + // cma-able / non-cma-able breakdown) and the CMA reservoir + // (free / other apps). Distinct negative pids keep the adapter's + // stable ids unique (the deficit row already owns pid 0). + // The deficit row owns the acquire buttons; when it is hidden but + // acquire still has work (a grown pool_want the reservoir can stage + // in, so nothing is "waiting to be acquired"), the CMA row shows them. + boolean acquireOnCma = cmaOn && deficitKb <= 0 && snap.deficit > 0; + if (snap.loaded) { + list.add(new HugePageProcess( + PID_AVAIL, getString(R.string.hugepage_bar_available), -1, + holdKb, '?', null, true, HugePageColor.availIcon(dark), + true, false, + availCmaAbleKb >= 0 ? getString( + R.string.hugepage_proc_avail_detail, + SizeUtils.formatSize(availCmaAbleKb * 1024), + SizeUtils.formatSize(availNonCmaKb * 1024)) : null, + false)); + } + if (cmaOn) { + list.add(new HugePageProcess( + PID_CMA, getString(R.string.hugepage_bar_cma), -1, + cmaKb, '?', null, true, HugePageColor.cmaFree(this), + true, false, + (cmaUsage != null && cmaUsage.ok) ? getString( + R.string.hugepage_proc_cma_detail, + SizeUtils.formatSize(cmaFreeKb * 1024), + SizeUtils.formatSize(cmaOtherKb * 1024)) : null, + acquireOnCma)); + } if (deficitKb > 0) { list.add(new HugePageProcess( 0, getString(R.string.hugepage_proc_deficit), -1, deficitKb, @@ -230,6 +276,10 @@ private void refresh() { var fKoNow = koNow; var fTraced = tracedKb; var fHold = holdKb; + var fAvailNonCma = availCmaAbleKb >= 0 ? availNonCmaKb : 0; + var fCma = cmaKb; + var fCmaFree = cmaFreeKb; + var fBarWant = barWantKb; var fWant = wantKb; var fAcquiring = kernelAcquiring; var fMode = kernelMode; @@ -249,7 +299,8 @@ private void refresh() { int uiMode = fAcquiring ? fMode : (acquireWatching ? acquireWatchMode : -1); adapter.setAcquireState(fAcquiring || acquireWatching, uiMode); - showResult(finalList, fTraced, fHold, fWant, finalEmpty); + showResult(finalList, fTraced, fHold, fAvailNonCma, + fCma, fCmaFree, fBarWant, fWant, finalEmpty); }); }); } @@ -262,12 +313,18 @@ private List buildFromEntries( // Best-effort pid -> VM name (running VMs only); for KO rows this labels // the process, for scan rows it echoes the name the entry already carries. var vmMap = model.vmNames(false); + // Rank-based colors over the full entry list (see HugePageColor). + var pids = new ArrayList(); + for (var e : entries) pids.add(e.pid); + var colorMap = HugePageColor.forPids(pids, dark); var result = new ArrayList(); for (var e : entries) { + Integer color = colorMap.get(e.pid); result.add(new HugePageProcess( e.pid, e.comm, -1, e.pages * 2048, e.state, vmMap.getOrDefault(e.pid, e.comm), e.alive, - HugePageColor.forPid(e.pid, dark), false, false)); + color != null ? color : HugePageColor.forRank(0, dark), + false, false)); } result.sort((a, b) -> Long.compare(b.thpKb, a.thpKb)); return result; @@ -275,17 +332,21 @@ private List buildFromEntries( private void showResult( @NonNull List list, long usedKb, long availKb, + long availNonCmaKb, long cmaKb, long cmaFreeKb, long barWantKb, long wantKb, @NonNull String emptyText ) { if (isFinishing()) return; firstRefresh = false; adapter.submit(list); - // Segmented bar (shared builder): VM segments (used), then the available - // portion as a track-coloured gap, then the "waiting for acquire" deficit - // pinned flush right. This screen's bar is a plain unlabelled meter. + // Segmented bar, synced with the status screen: VM segments (used), the + // available block (with its non-cma-able left sub-split), the CMA + // reservoir block ([free|other apps] sub-split), then the "waiting for + // acquire" deficit flush right. Still a plain unlabelled meter here. + // The synthetic available/CMA rows ("unknown") are drawn via those + // dedicated blocks, not as used segments. int used = 0; - for (var p : list) if (!p.acquire) used++; + for (var p : list) if (!p.acquire && !p.unknown) used++; int[] usedColors = new int[used]; float[] usedValues = new float[used]; int deficitColor = HugePageColor.pending(this); @@ -295,14 +356,26 @@ private void showResult( if (p.acquire) { // deficit rows: colour + value deficitColor = p.color; deficit += Math.max(0, p.thpKb); - } else { // used: one segment per VM + } else if (!p.unknown) { // used: one segment per VM usedColors[u] = p.color; usedValues[u] = Math.max(0, p.thpKb); u++; } } - segBar.setStorage(usedColors, usedValues, null, - availKb, null, deficitColor, deficit, null, wantKb); + var spec = new SegmentedBar.StorageSpec(); + spec.usedColors = usedColors; + spec.usedValues = usedValues; + spec.avail = availKb; + spec.availNonCma = availNonCmaKb; + spec.availNonCmaColor = HugePageColor.availNonCma(this); + spec.cmaFree = cmaFreeKb; + spec.cmaFreeColor = HugePageColor.cmaFree(this); + spec.cmaOther = cmaKb - cmaFreeKb; + spec.cmaOtherColor = HugePageColor.cmaUsed(this); + spec.deficitColor = deficitColor; + spec.deficit = deficit; + spec.want = barWantKb; + segBar.setStorage(spec); // 2x2 caption: used / available on top, total / pool-size below. // Total = the real held reserve (used + avail = owned + traced), shown @@ -417,15 +490,11 @@ public void onAcquire(int mode) { if (finalSnap != null && finalSnap.loaded) { // Achieved = owned + traced (pool_avail + served), NOT // pool_total: after a shrink that left served pages out, - // pool_total under-counts the pages VMs already hold. - long got = finalSnap.free + finalSnap.lent; - long want = finalSnap.targetIdeal; - String msg = got >= want - ? getString(R.string.hugepage_proc_acquire_full, - fmtPages(want)) - : getString(R.string.hugepage_proc_acquire_partial, - fmtPages(got), fmtPages(want)); - Toast.makeText(this, msg, LENGTH_LONG).show(); + // pool_total under-counts the pages VMs already hold. With + // the reservoir on the message reports the with-CMA total too. + Toast.makeText(this, + HugePageActivity.acquireDoneMessage(this, finalSnap), + LENGTH_LONG).show(); } else { Toast.makeText(this, R.string.hugepage_proc_acquire_done, LENGTH_SHORT).show(); diff --git a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcessAdapter.java b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcessAdapter.java index 39f138899..1e3206493 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcessAdapter.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcessAdapter.java @@ -111,17 +111,24 @@ public void onBindViewHolder(@NonNull Holder holder, int position) { } if (p.unknown || p.acquire) { - // Synthetic rows: "unattributed" (no action) and "waiting for - // acquire" (deficit, with an Acquire button). + // Synthetic rows: "available"/"CMA"/"unattributed" (no action) and + // "waiting for acquire" (deficit, with an Acquire button). The + // served line doubles as their free-text detail (breakdowns). holder.title.setText(p.comm); - holder.served.setVisibility(GONE); + if (p.detail != null) { + holder.served.setVisibility(VISIBLE); + holder.served.setText(p.detail); + } else { + holder.served.setVisibility(GONE); + } holder.stateView.setVisibility(GONE); holder.btnStack.setVisibility(GONE); holder.btnStack.setOnClickListener(null); holder.itemView.setAlpha(1f); holder.btnKill.setVisibility(GONE); holder.btnKill.setOnClickListener(null); - if (p.acquire) { + // Usually the deficit row; the CMA row when that one is hidden. + if (p.acquireSlots) { bindAcquireSlot(holder.btnAcquireV1, holder.progressAcquireV1, 1); bindAcquireSlot(holder.btnAcquireV2, holder.progressAcquireV2, 2); bindAcquireSlot(holder.btnAcquireV3, holder.progressAcquireV3, 3); diff --git a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/SegmentedBar.java b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/SegmentedBar.java index a5c864ffd..e81bcd868 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/SegmentedBar.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/SegmentedBar.java @@ -30,6 +30,13 @@ public final class SegmentedBar extends View { private int[] colors = new int[0]; private float[] values = new float[0]; + // Optional per-segment sub-split: the LEFT subLeftValues[i] share of segment + // i is drawn in subLeftColors[i], the rest in colors[i]. Pure color detail - + // the segment stays ONE logical block with ONE (centred) label. + @Nullable + private float[] subLeftValues = null; + @Nullable + private int[] subLeftColors = null; @Nullable private String[] labels = null; // Per-segment label data pre-split, pre-measured and pre-coloured in setData, @@ -78,9 +85,19 @@ public void setData(@NonNull int[] colors, @NonNull float[] values, float total) /** Apple-style: each wide-enough segment draws {@code labels[i]} inside it. */ public void setData(@NonNull int[] colors, @NonNull float[] values, @Nullable String[] labels, float total) { + setData(colors, values, labels, null, null, total); + } + + /** Full form: labels plus optional per-segment left sub-splits (see fields). */ + public void setData(@NonNull int[] colors, @NonNull float[] values, + @Nullable String[] labels, + @Nullable float[] subLeftValues, @Nullable int[] subLeftColors, + float total) { this.colors = colors; this.values = values; this.labels = labels; + this.subLeftValues = subLeftValues; + this.subLeftColors = subLeftColors; this.total = total; prepareLabels(); invalidate(); @@ -116,6 +133,36 @@ private void prepareLabels() { } } + /** + * Parameters for the storage-style bar - one holder instead of the long + * positional argument list. Only {@code usedColors}/{@code usedValues}/ + * {@code want} are always meaningful; the CMA and {@code availNonCma} fields + * stay 0 for the classic [used][available][waiting] bar. + */ + public static final class StorageSpec { + /** one colour per used segment */ + @NonNull public int[] usedColors = new int[0]; + /** one value per used segment (same length as usedColors) */ + @NonNull public float[] usedValues = new float[0]; + /** per-segment labels, or {@code null} for the plain (unlabelled) meter */ + @Nullable public String[] usedLabels; + public float avail; + @Nullable public String availLabel; + /** left sub-split of the available block (non-cma-able share) */ + public float availNonCma; + public int availNonCmaColor; + public float cmaFree; + @Nullable public String cmaFreeLabel; + public int cmaFreeColor; + public float cmaOther; + @Nullable public String cmaOtherLabel; + public int cmaOtherColor; + public int deficitColor; + public float deficit; + @Nullable public String deficitLabel; + public float want; + } + /** * Assemble and set a storage-style bar shared by both hugepage screens: * the {@code used} segments, then the available portion as a track-coloured @@ -135,26 +182,59 @@ public void setStorage( int deficitColor, float deficit, @Nullable String deficitLabel, float want ) { - int n = usedValues.length; - boolean withLabels = usedLabels != null; - int[] c = new int[n + 2]; - float[] v = new float[n + 2]; - String[] l = withLabels ? new String[n + 2] : null; + var s = new StorageSpec(); + s.usedColors = usedColors; + s.usedValues = usedValues; + s.usedLabels = usedLabels; + s.avail = avail; + s.availLabel = availLabel; + s.deficitColor = deficitColor; + s.deficit = deficit; + s.deficitLabel = deficitLabel; + s.want = want; + setStorage(s); + } + + /** + * v10 variant of {@link #setStorage}: the bar reads + * [VMs][available][CMA free][CMA lent][waiting]. The two CMA parts are + * ordinary labelled segments; only available carries the pure-color + * left sub-split [non-cma-able | normal] (its left {@code availNonCma} + * share draws in {@code availNonCmaColor}) under a single label. Leave the + * CMA values and {@code availNonCma} at 0 for the classic bar (the plain + * overload does exactly that). + */ + public void setStorage(@NonNull StorageSpec s) { + int n = s.usedValues.length; + boolean withLabels = s.usedLabels != null; + int[] c = new int[n + 4]; + float[] v = new float[n + 4]; + String[] l = withLabels ? new String[n + 4] : null; + float[] sv = new float[n + 4]; + int[] sc = new int[n + 4]; float seg = 0; for (int i = 0; i < n; i++) { - c[i] = usedColors[i]; - v[i] = Math.max(0, usedValues[i]); - if (withLabels) l[i] = usedLabels[i]; + c[i] = s.usedColors[i]; + v[i] = Math.max(0, s.usedValues[i]); + if (withLabels) l[i] = s.usedLabels[i]; seg += v[i]; } - float a = Math.max(0, avail); - c[n] = trackColor; // available gap + float a = Math.max(0, s.avail); + c[n] = trackColor; // available: [non-cma-able|normal] v[n] = a; - if (withLabels) l[n] = availLabel; - c[n + 1] = deficitColor; // deficit, flush right - v[n + 1] = Math.max(0, deficit); - if (withLabels) l[n + 1] = deficitLabel; - setData(c, v, l, Math.max(want, seg + a)); + sv[n] = Math.min(a, Math.max(0, s.availNonCma)); + sc[n] = s.availNonCmaColor; + if (withLabels) l[n] = s.availLabel; + c[n + 1] = s.cmaFreeColor; // CMA free in buddy + v[n + 1] = Math.max(0, s.cmaFree); + if (withLabels) l[n + 1] = s.cmaFreeLabel; + c[n + 2] = s.cmaOtherColor; // CMA lent to other apps + v[n + 2] = Math.max(0, s.cmaOther); + if (withLabels) l[n + 2] = s.cmaOtherLabel; + c[n + 3] = s.deficitColor; // deficit, flush right + v[n + 3] = Math.max(0, s.deficit); + if (withLabels) l[n + 3] = s.deficitLabel; + setData(c, v, l, sv, sc, Math.max(s.want, seg + a + v[n + 1] + v[n + 2])); } @Override @@ -181,8 +261,19 @@ protected void onDraw(@NonNull Canvas canvas) { float segW = w * (values[i] / total); if (segW <= 0f) continue; float segRight = Math.min(x + segW, w); + // Optional left sub-split: same logical segment, two fill colors. + float subW = 0f; + if (subLeftValues != null && subLeftColors != null + && i < subLeftValues.length && i < subLeftColors.length + && subLeftValues[i] > 0f && values[i] > 0f) { + subW = Math.min(segW, segW * (subLeftValues[i] / values[i])); + } + if (subW > 0f) { + paint.setColor(subLeftColors[i]); + canvas.drawRect(x, 0, Math.min(x + subW, segRight), h, paint); + } paint.setColor(colors[i]); - canvas.drawRect(x, 0, segRight, h, paint); + canvas.drawRect(Math.min(x + subW, segRight), 0, segRight, h, paint); drawLabel(canvas, i, x, segRight, h); x += segW; } diff --git a/app/src/main/res/layout/activity_hugepage.xml b/app/src/main/res/layout/activity_hugepage.xml index 44b571294..c8191a597 100644 --- a/app/src/main/res/layout/activity_hugepage.xml +++ b/app/src/main/res/layout/activity_hugepage.xml @@ -113,17 +113,42 @@ - + + android:orientation="horizontal"> + + + + + + + #FF64B5F6 #80BFAF7C + #80D9A06B + #807CC7BD + #5995A7B8 diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index b577e3fda..5019615d3 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -929,8 +929,11 @@ 已回收次数 活动虚拟机 可用: %1$d (%2$s) + 可转换CMA %1$s、不可转换CMA %2$s + 空闲 %1$s、其他 App %2$s 总共: %1$d (%2$s) 池大小: %1$d (%2$s) + 池大小:%1$d/%2$d(%3$s / %4$s) 模块未加载 回收 回收已触发 @@ -994,12 +997,58 @@ 停止获取 获取完成:已达成 %1$s 获取结束:达成 %1$s / 目标 %2$s(系统目前无法再凑出更多) + 获取完成:池 %1$s、含 CMA %2$s + 获取停止:池 %1$s / %2$s,含 CMA %3$s / %4$s(系统目前无法再搬移) 获取大页中… 模块不足,尝试 userspace 协助… 占用:%1$d 页 (%2$s) 等待获取 pid %1$d 可用 + + + 启用 CMA + 闲置保留以 CMA 形式借给 App 使用,通过获取取回 + 含 CMA 池大小 + 模块未加载或不支持 CMA(需 v10+) + CMA 不可用 + 本次开机模块已停用 CMA 功能(内核预检/符号解析/首块验证未通过)。 + 先前探测曾判定 CMA 不可用,该记录会让模块整个开机期间关闭 CMA。记录已清除,请重启后再次开启 CMA 以重新探测。 + 写入 pool_want_with_cma 被拒:%1$s + 无法组出任何 CMA 料场(headroom floor 拒绝,或区块翻转失败)。 + 需要先进行 CMA 探测 + 首次使用前需探测该厂商内核是否允许 App 使用 CMA 内存。\n\n探测会暂时清空池、把它整块转成 CMA 料场(目标约 %1$s),用 balloon 工具施加内存压力,再按 CMA 消耗量判读。结束后会还原池,进行前所有 VM 需先关闭。\n\n探测通过后,池大小会设为 %2$s,其余保留为料场借给 App,直到 VM 需要时才取回。 + 开始探测 + 直接进行探测 + 完成探测 + 料场会偏小 + 该模块总共只能监护 %1$s,少于探测想要的 %2$s,判读结果可能不准确。仍要探测吗? + 尚未完成的 CMA 探测 + 已建立 CMA 料场但还没判读。要现在执行 balloon 测量,确认 App 是否真的用得到吗? + 建立 CMA 料场中\u2026 %1$s / %2$s + 料场未达目标 + 只组出 %1$s / %2$s,运行期的 sweep 撞到碎片墙。刚开机时内存没有碎片,同一个流程就会成功;可以重启后再次开启 CMA。或者直接用已组出的 %1$s 进行判读(可能较不准确)。 + CMA 已标记为不可用 + 先前探测结果:该设备的 App 无法使用 CMA,启用只会浪费保留池。要重新探测吗? + 重新探测 + 请先关闭所有运行中的 VM 再探测 + 我重启后再探测 + 未变更任何设置。请重启后再次开启 CMA:刚开机时内存没有碎片,料场就组得起来。 + 正在施加内存压力(balloon)\u2026 可能需要数分钟 + 探测通过:App 可使用 CMA。已启用,池大小设为 %1$s,其余作为料场借出。 + 探测结果:该设备的 App 无法使用 CMA,启用只会浪费保留池。仍要启用吗?您的选择会被记录,之后不再探测。 + 判读结果异常 + 压力共消耗 CMA %1$s / 池 %2$s(balloon 持有 %3$s,停止原因:%4$s),无法明确判读。仍要启用 CMA 吗? + 启用 + 保持关闭 + 已取消探测 + CMA 探测无法运行 + CMA 已启用 + CMA 已启用,池大小设为 %1$s,其余作为料场借出。 + CMA 已停用 + 写入 CMA 目标失败 + CMA + CMA(借出) 目前没有线程占用保留的 hugepage 池。 终止 发送信号失败 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index d6d5a0956..5300dd504 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -929,9 +929,12 @@ 已回收次數 活動虛擬機器 可用: %1$d (%2$s) + 可轉換CMA %1$s、不可轉換CMA %2$s + 空閒 %1$s、其他 App %2$s 已使用: %1$d (%2$s) 總共: %1$d (%2$s) 池大小: %1$d (%2$s) + 池大小:%1$d/%2$d(%3$s / %4$s) 模組未載入 獲取大頁 獲取已觸發 @@ -951,6 +954,8 @@ 停止獲取 獲取完成:已達成 %1$s 獲取結束:達成 %1$s / 目標 %2$s(系統目前無法再湊出更多) + 獲取完成:池 %1$s、含 CMA %2$s + 獲取停止:池 %1$s / %2$s,含 CMA %3$s / %4$s(系統目前無法再搬移) 模組不足,嘗試 userspace 協助… 觸發獲取失敗 獲取大頁 @@ -988,6 +993,50 @@ 等待獲取 pid %1$d 可用 + + + 啟用 CMA + 閒置保留以 CMA 形式借給 App 使用,透過獲取取回 + 含 CMA 池大小 + 模組未載入或不支援 CMA(需 v10+) + CMA 不可用 + 本次開機模組已停用 CMA 功能(kernel 預檢/符號解析/首塊驗證未通過)。 + 先前探測曾判定 CMA 不可用,該紀錄會讓模組整個開機期間關閉 CMA。紀錄已清除,請重新開機後再次開啟 CMA 以重新探測。 + 寫入 pool_want_with_cma 被拒:%1$s + 無法組出任何 CMA 料場(headroom floor 拒絕,或區塊翻轉失敗)。 + 需要先進行 CMA 探測 + 首次使用前需探測此廠商核心是否允許 App 使用 CMA 記憶體。\n\n探測會暫時清空池、把它整塊轉成 CMA 料場(目標約 %1$s),用 balloon 工具施加記憶體壓力,再依 CMA 消耗量判讀。結束後會還原池,進行前所有 VM 需先關閉。\n\n探測通過後,池大小會設為 %2$s,其餘保留為料場借給 App,直到 VM 需要時才取回。 + 開始探測 + 直接進行探測 + 完成探測 + 料場會偏小 + 此模組總共只能監護 %1$s,少於探測想要的 %2$s,判讀結果可能不準確。仍要探測嗎? + 尚未完成的 CMA 探測 + 已建立 CMA 料場但還沒判讀。要現在執行 balloon 量測,確認 App 是否真的用得到嗎? + 建立 CMA 料場中\u2026 %1$s / %2$s + 料場未達目標 + 只組出 %1$s / %2$s,執行期的 sweep 撞到碎片牆。剛開機時記憶體沒有碎片,同一個流程就會成功;可以重新開機後再次開啟 CMA。或者直接用已組出的 %1$s 進行判讀(可能較不準確)。 + CMA 已標記為不可用 + 先前探測結果:此裝置的 App 無法使用 CMA,啟用只會浪費保留池。要重新探測嗎? + 重新探測 + 請先關閉所有執行中的 VM 再探測 + 我重開機後再探測 + 未變更任何設定。請重新開機後再次開啟 CMA:剛開機時記憶體沒有碎片,料場就組得起來。 + 正在施加記憶體壓力(balloon)\u2026 可能需要數分鐘 + 探測通過:App 可使用 CMA。已啟用,池大小設為 %1$s,其餘作為料場借出。 + 探測結果:此裝置的 App 無法使用 CMA,啟用只會浪費保留池。仍要啟用嗎?您的選擇會被記錄,之後不再探測。 + 判讀結果異常 + 壓力共消耗 CMA %1$s / 料場 %2$s(balloon 持有 %3$s,停止原因:%4$s),無法明確判讀。仍要啟用 CMA 嗎? + 啟用 + 維持關閉 + 已取消探測 + CMA 探測無法執行 + CMA 已啟用 + CMA 已啟用,池大小設為 %1$s,其餘作為料場借出。 + CMA 已停用 + 寫入 CMA 目標失敗 + CMA + CMA(借出) 虛擬機器:%1$s 非追蹤中的虛擬機器 狀態:%1$c (%2$s) diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index e22a01568..c01c22999 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -25,6 +25,11 @@ #33808080 #807D9CB3 + + #80C98B5E + #805FA79E + + #59708090 #FF4CAF50 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 85f08a344..59d01665c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -966,9 +966,12 @@ Total Refilled Active VMs Available: %1$d (%2$s) + CMA convertible %1$s, non-convertible %2$s + free %1$s, other apps %2$s Used: %1$d (%2$s) Total: %1$d (%2$s) Pool size: %1$d (%2$s) + Pool size: %1$d/%2$d (%3$s / %4$s) Module not loaded Acquire Pages Acquire triggered @@ -989,6 +992,8 @@ Stop acquiring Acquire complete: reached %1$s Acquire stopped: got %1$s of %2$s (system can\'t migrate more right now) + Acquire complete: pool %1$s, with CMA %2$s + Acquire stopped: pool %1$s of %2$s, with CMA %3$s of %4$s (system can\'t migrate more right now) Module short; trying userspace assist... Acquire huge pages Pool Size @@ -1025,6 +1030,50 @@ Waiting for acquire pid %1$d Available + + + Enable CMA + Lend the idle reserve to apps as CMA; reclaimed by acquire + Pool Size with CMA + Module not loaded or has no CMA support (v10+) + CMA unavailable + The module disabled its CMA side this boot (kernel preflight / symbol resolution / first-block verification failed). + A previous probe recorded CMA as unusable, which keeps the module\'s CMA side off for the whole boot. That record has been cleared - reboot, then enable CMA again to re-probe. + Writing pool_want_with_cma was rejected: %1$s + No CMA reservoir could be assembled (the headroom floor refused it, or block flipping failed). + CMA probe required + Before first use, a probe must verify this vendor kernel lets apps allocate from CMA.\n\nIt temporarily empties the pool into a CMA reservoir (aiming for %1$s), applies memory pressure with the balloon tool, then judges from how much CMA got consumed. The pool is restored afterwards, and all VMs must be stopped first.\n\nOn success the pool is set to %2$s and the rest is kept as reservoir, lent to apps until a VM needs it. + Start probe + Probe anyway + Finish probe + Reservoir will be small + This module can only guard %1$s in total, less than the %2$s the probe wants to measure against, so the verdict may be unreliable. Probe anyway? + Unfinished CMA probe + A CMA reservoir was set up but never measured. Run the balloon measurement now to decide whether apps can really use it? + Building the CMA reservoir\u2026 %1$s / %2$s + Reservoir came up short + Assembled %1$s of %2$s - the runtime sweep hit the fragmentation wall. On a fresh boot memory is unfragmented, so the same run succeeds; reboot and turn CMA on again. Or measure right now against the %1$s that did assemble (the verdict may be less reliable). + CMA marked unusable + A previous probe found apps cannot use CMA on this device, so enabling it would only waste the reserve. Run the probe again? + Re-probe + Stop all running VMs before probing + I\'ll reboot, then probe + Nothing was changed. Reboot, then turn CMA on again - on a fresh boot memory is unfragmented and the reservoir assembles. + Applying memory pressure (balloon)\u2026 this can take several minutes + Probe passed: apps can use CMA. CMA enabled and the pool set to %1$s - the rest is lent out as reservoir. + Probe result: apps could not use CMA on this device, so enabling it would only waste the reserve. Enable anyway? Your choice is recorded and the probe will not run again. + Probe result unreadable + Pressure consumed %1$s of the %2$s reservoir (balloon held %3$s, stopped: %4$s). This matches neither verdict. Enable CMA anyway? + Enable + Keep off + Probe cancelled + CMA probe could not run + CMA enabled + CMA enabled and the pool set to %1$s - the rest is lent out as reservoir. + CMA disabled + Failed to write the CMA target + CMA + CMA (lent) VM: %1$s Not a tracked VM State: %1$c (%2$s) From d70c4456fb3f15f94346b15758cfae1d7e193eac Mon Sep 17 00:00:00 2001 From: HuJK Date: Mon, 13 Jul 2026 00:17:21 +0800 Subject: [PATCH 120/182] hugepage: adapt to v11 module (movable->CMA levers, drop probe) The v11 gh_hugepage_reserve module can open the movable->CMA redirect directly, so the app no longer needs the balloon consumability probe to decide whether apps can use the reservoir. Remove the probe subsystem entirely: runCmaProbe and all of its dialog/worker plumbing, the balloon runner, the cma_probe_result verdict, and the reboot-to-finish pending prompt. New flow when the CMA switch is turned on: - if moveable_to_cma_vender_already_allowed reads 1, the vendor kernel already redirects movable->CMA: enable directly, no risk; otherwise warn, then offer two named actions: * "Remove CMA Restriction" arms a lever LIVE ONLY (can crash/reboot the phone). On 6.1 the restrict_cma_redirect flag is side-effect-free so it is tried first (falling back to the gfp hook); on 6.6/6.12 the same key also backs cma_has_pcplist(), so the narrower gfp hook is used directly; * "Module CMA" builds the reservoir without arming any lever - no crash risk; the reserve still serves VMs via stage-in, and apps consume it only where the vendor kernel already allows movable->CMA; - once it is running, offer to save the setting to settings.prop for next boot. Splitting apply from save is the safety net: a live apply that crashes leaves nothing persisted, so the next boot comes up clean. The persisted key (cma_movable_lever) is app-owned; a future load.sh reads it. Disabling reverts whichever lever was armed (the module no-ops these when the vendor already redirects) and forgets the saved choice. Pool-size UI: the pool size and the with-CMA total are now two stacked rows instead of a cramped side-by-side pair; the with-CMA row is greyed and non-editable while CMA is off. A stale cma_probe_result from an older app version is cleared on screen load so the unchanged boot script doesn't keep the module's CMA side cold. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01ARourbDczkCYsKUUMC9EFv --- .../droidvm/ui/hugepage/HugePageActivity.java | 772 ++++-------------- .../droidvm/ui/hugepage/HugePageModel.java | 199 ++--- app/src/main/res/layout/activity_hugepage.xml | 18 +- app/src/main/res/values-zh-rCN/strings.xml | 40 +- app/src/main/res/values-zh-rTW/strings.xml | 40 +- app/src/main/res/values/strings.xml | 40 +- 6 files changed, 291 insertions(+), 818 deletions(-) diff --git a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageActivity.java index 66b7bd6ea..261e1c59f 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageActivity.java @@ -39,8 +39,6 @@ import com.google.android.material.progressindicator.LinearProgressIndicator; import android.text.Editable; -import android.widget.LinearLayout; -import android.widget.ProgressBar; import java.math.BigInteger; import java.util.ArrayList; @@ -49,8 +47,6 @@ import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; import cn.classfun.droidvm.R; import cn.classfun.droidvm.lib.daemon.DaemonConnection; @@ -115,37 +111,13 @@ public final class HugePageActivity extends AppCompatActivity { private SwitchRowWidget rowCmaEnable; private boolean cmaSwitchSyncing = false; // programmatic setChecked guard private boolean cmaInputLoaded = false; // seed the CMA size input once per show - private boolean cmaBusy = false; // a probe / toggle flow is in flight - // A reservoir target persisted with no verdict recorded = a probe that asked - // for a reboot and is now waiting to be finished. Offer it once per screen. - private boolean probePromptShown = false; + private boolean cmaBusy = false; // an enable/disable flow is in flight // Two-way size link (pool_want <= pool_want_with_cma): which input the // user touched last decides who yields when they cross. private static final int SIZE_EDIT_POOL = 1; private static final int SIZE_EDIT_CMA = 2; private int lastSizeEdit = SIZE_EDIT_POOL; private boolean sizeLinkSyncing = false; // programmatic setBigValue guard - /** Balloon floor (MB) for the consumability probe - `balloon 1536`. */ - private static final long BALLOON_FLOOR_MB = 1536; - private static final long BALLOON_TIMEOUT_S = 600; - /** - * How much reservoir the probe wants to measure against: - * {@code max(RAM - 8G, RAM * 0.4)}. RAM here is MemTotal - physical pages - * only, so zram/swap capacity (SwapTotal) never inflates it. Advisory, not - * a precondition: a smaller reservoir still probes, it just makes the - * verdict less reliable, and the user is warned before continuing. - */ - private static final long PROBE_KEEP_BYTES = 8L << 30; // 8 GiB - private static final double PROBE_MIN_RAM_FRACTION = 0.4; - /** - * Pool size a passing probe leaves behind. The probe has just proved apps - * can consume the reservoir, so holding a large pool would waste memory the - * reservoir would otherwise lend out: pin the pool small and let - * pool_want_with_cma (kept at the size the probe assembled) carry the rest - * as reservoir. A VM start stages pages back in on demand. - */ - private static final long PROBE_POOL_BYTES = 512L << 20; // 512 MiB - private static final long PROBE_POOL_PAGES = PROBE_POOL_BYTES / PAGE_SIZE; private TextRowWidget rowStatState; private TextRowWidget rowStatTotalServed; private TextRowWidget rowStatTotalRefilled; @@ -253,6 +225,10 @@ public void afterTextChanged(Editable s) { applyAcquireState(); cardCrashWarning.setOnClickListener(v -> doDismissCrash()); loadPoolSize(); + // One-time migration: drop a stale cma_probe_result from the removed + // probe, which the shipped boot script would otherwise treat as a denial + // and keep the module's CMA side cold. + runOnPool(model::clearLegacyProbeKey); } /** @@ -460,14 +436,8 @@ private void refreshStatus() { ? new LinkedHashMap<>() : model.vmNames(false); // Reservoir occupancy for the two-tone CMA block (module caches ~1s). var cmaUsage = snap.cmaActive() ? model.cmaUsage() : null; - // A reservoir built (or being built) toward a target nobody ever - // judged: the "save and reboot" branch of the probe left it here. - boolean probePending = !probePromptShown && !cmaBusy - && snap.cmaActive() && model.cmaProbeResult() == null; - runOnUiThread(() -> { - updateUI(snap, crashStamp, owners, allPids, vmMap, cmaUsage); - if (probePending) promptPendingProbe(); - }); + runOnUiThread(() -> + updateUI(snap, crashStamp, owners, allPids, vmMap, cmaUsage)); }); } @@ -673,9 +643,9 @@ private void updateUI( acquireEnabled = snap.loaded && snap.deficit > 0; applyAcquireState(); - // CMA switch: only meaningful on a loaded v10 module. While a probe / - // toggle flow runs, leave the switch and the size input alone - the flow - // owns them (the reservoir flips around mid-probe and would flicker). + // CMA switch: only meaningful on a loaded v10+ module. While an enable/ + // disable flow runs, leave the switch and the size inputs alone - the + // flow owns them (the reservoir flips around mid-flow and would flicker). rowCmaEnable.setEnabled(snap.loaded && snap.hasCma); if (!cmaBusy) { boolean cmaActive = snap.cmaActive(); @@ -684,14 +654,12 @@ private void updateUI( rowCmaEnable.setChecked(cmaActive); cmaSwitchSyncing = false; } - // The size row shows exactly one GiB tag: on the right field while - // CMA is on (two fields, tight width), on the pool field otherwise. - inputPoolSize.setUnitButtonVisible(!cmaActive); + // Two stacked size rows: the with-CMA total is editable only while + // the reservoir is on, greyed and non-editable otherwise. Seed it + // from pool_want_with_cma once each time CMA turns on. + inputCmaSize.setEnabled(cmaActive); if (cmaActive) { - // Seed the with-CMA total input once per show (it maps 1:1 to - // pool_want_with_cma), then leave the user's typing be. - if (inputCmaSize.getVisibility() != VISIBLE || !cmaInputLoaded) { - inputCmaSize.setVisibility(VISIBLE); + if (!cmaInputLoaded) { sizeLinkSyncing = true; try { inputCmaSize.setBigValue( @@ -702,7 +670,6 @@ private void updateUI( cmaInputLoaded = true; } } else { - inputCmaSize.setVisibility(GONE); cmaInputLoaded = false; } } @@ -854,7 +821,7 @@ private long runningVmMemMib() { * shrinks the pool. Runs on focus-loss of either field and again at save. */ private void reconcileSizeLink() { - if (inputCmaSize.getVisibility() != VISIBLE) return; + if (!inputCmaSize.isEnabled()) return; // only linked while CMA is on if (!inputPoolSize.isInputValid() || !inputCmaSize.isInputValid()) return; var pool = inputPoolSize.getBigValue(); var withCma = inputCmaSize.getBigValue(); @@ -873,10 +840,10 @@ private void savePoolSize() { if (!inputPoolSize.isInputValid()) return; var bytes = inputPoolSize.getBigValue(); var pages = bytes.divide(BigInteger.valueOf(PAGE_SIZE)); - // While the reservoir is on, the right field IS the with-CMA total + // While the reservoir is on, the with-CMA row IS the with-CMA total // (pool_want_with_cma); the link above already keeps it >= the pool. final long cmaPages; - if (inputCmaSize.getVisibility() == VISIBLE) { + if (inputCmaSize.isEnabled()) { if (!inputCmaSize.isInputValid()) return; cmaPages = inputCmaSize.getBigValue() .divide(BigInteger.valueOf(PAGE_SIZE)).longValue(); @@ -938,7 +905,7 @@ private void doToggleModule() { } /* ================================================================== */ - /* v10 CMA reservoir: switch + consumability probe */ + /* v11 CMA reservoir: switch + movable->CMA levers */ /* ================================================================== */ private void onCmaSwitchChanged(boolean checked) { @@ -965,10 +932,18 @@ private void cancelCmaEnable() { refreshStatus(); } - /** Switch off: demolish the reservoir now and persist off for next boot. */ + /** + * Switch off: revert whichever movable->CMA lever we armed (the module + * no-ops these writes when the vendor kernel already redirects, so this is + * safe in every case), demolish the reservoir now, and forget the saved + * lever so a future boot doesn't re-apply it. + */ private void doCmaDisable() { cmaBusy = true; runOnPool(() -> { + model.setGfpHook(false); + model.setRestrictFlip(false); + model.clearCmaLever(); var res = model.saveCmaTarget(0); runOnUiThread(() -> { cmaBusy = false; @@ -981,12 +956,15 @@ private void doCmaDisable() { } /** - * Switch on. The magisk-side {@code cma_probe_result} (settings.prop) says - * whether the consumability probe ever ran: + * Switch on. Plain movable allocations only reach the reservoir if the + * kernel redirects movable->CMA: *

      - *
    • {@code 1} - apps can consume CMA: enable directly, no probe;
    • - *
    • {@code 0} - probed unusable: offer a re-probe;
    • - *
    • absent - never probed: explain and offer to run it.
    • + *
    • the vendor kernel already redirects - enable directly, no risk;
    • + *
    • otherwise a lever must be armed, which can destabilise the kernel - + * warn, then arm it live only ({@link #tryCmaLever}); on success + * offer to save it ({@link #promptSaveLever}). Splitting arm from save + * is the safety net: if arming crashes the phone, nothing was saved and + * the next boot comes up clean.
    • *
    */ private void doCmaEnable() { @@ -1002,83 +980,57 @@ private void doCmaEnable() { }); return; } - var verdict = model.cmaProbeResult(); if (snap.cmaPbOrder < 0) { - // The module disabled its whole CMA side this boot (preflight / - // symbols / first-block verification) - no write can help now. - // A recorded denial is one of the causes (the boot script then - // hands the module -1 preflight values): drop it, so the next - // boot comes up CMA-capable and the probe can run again. - boolean stale = verdict != null && verdict == VERDICT_DENIED; - if (stale) model.clearCmaProbeResult(); + // The module turned its whole CMA side off this boot (preflight / + // symbols / first-block verification) - no lever can help now. runOnUiThread(() -> { cmaBusy = false; setCmaSwitch(false); if (isFinishing()) return; new MaterialAlertDialogBuilder(this) .setTitle(R.string.hugepage_cma_unavailable_title) - .setMessage(stale ? R.string.hugepage_cma_unavailable_denied - : R.string.hugepage_cma_unavailable_boot) + .setMessage(R.string.hugepage_cma_unavailable_boot) .setPositiveButton(android.R.string.ok, null) .show(); }); return; } - // The threshold only feeds the two dialog branches - don't pay the - // meminfo shell read when the verdict lets us enable directly. - var needBytes = (verdict != null && verdict == VERDICT_ALLOWED) - ? 0 : probeNeedBytes(model.memTotalKb()); + if (model.mtcVenderAllowed() == 1) { + // Vendor already redirects movable->CMA: nothing to arm, no risk. + enableCmaDirect(snap); + return; + } + // A lever is required, and arming it can crash the device: warn first. runOnUiThread(() -> { if (isFinishing()) { cmaBusy = false; return; } - if (verdict != null && verdict == VERDICT_ALLOWED) { - enableCmaDirect(snap); - } else if (verdict != null && verdict == VERDICT_DENIED) { - new MaterialAlertDialogBuilder(this) - .setTitle(R.string.hugepage_cma_probe_denied_title) - .setMessage(R.string.hugepage_cma_probe_denied_msg) - .setPositiveButton(R.string.hugepage_cma_probe_rerun, - (d, w) -> startCmaProbe()) - .setNegativeButton(android.R.string.cancel, - (d, w) -> cancelCmaEnable()) - .setOnCancelListener(d -> cancelCmaEnable()) - .show(); - } else { - new MaterialAlertDialogBuilder(this) - .setTitle(R.string.hugepage_cma_probe_needed_title) - .setMessage(getString(R.string.hugepage_cma_probe_needed_msg, - SizeUtils.formatSize(Math.max(0, needBytes)), - SizeUtils.formatSize(PROBE_POOL_BYTES))) - .setPositiveButton(R.string.hugepage_cma_probe_start, - (d, w) -> startCmaProbe()) - .setNegativeButton(android.R.string.cancel, - (d, w) -> cancelCmaEnable()) - .setOnCancelListener(d -> cancelCmaEnable()) - .show(); - } + new MaterialAlertDialogBuilder(this) + .setTitle(R.string.hugepage_cma_warn_title) + .setMessage(R.string.hugepage_cma_warn_msg) + .setPositiveButton(R.string.hugepage_cma_remove_restriction, + (d, w) -> tryCmaLever(snap)) + .setNeutralButton(R.string.hugepage_cma_module_cma, + (d, w) -> enableCmaReservoirOnly(snap)) + .setNegativeButton(android.R.string.cancel, + (d, w) -> cancelCmaEnable()) + .setOnCancelListener(d -> cancelCmaEnable()) + .show(); }); }); } /** - * Probe already passed: restore the remembered with-CMA total and let a v3 - * acquire build the reservoir (only the mode-2/3 sweep runs Phase R; mode 1 - * is pool-only legacy). - * - *

    The target is clamped to the module's {@code pool_want <= - * pool_want_with_cma} invariant, so a remembered total at or below the pool - * size enables with an empty reservoir - that is a real state (the probe - * itself produces it when the pool already holds everything), and the user - * then raises the now-visible with-CMA field. Only {@code 0} is impossible: - * it is the off sentinel. + * Vendor already redirects movable->CMA (or a saved lever is already live): + * just set the with-CMA total and let a v3 acquire build the reservoir (only + * the mode-2/3 sweep runs Phase R; mode 1 is pool-only legacy). This path + * carries no crash risk, so the target is persisted and there is no separate + * save step. */ private void enableCmaDirect(@NonNull HugePageModel.Snapshot snap) { runOnPool(() -> { - long target = Math.max(model.lastCmaTargetPages(), snap.targetIdeal); - if (target <= 0) // pool soft-disabled: fall back to the probe floor - target = pagesFor(Math.max(0, probeNeedBytes(model.memTotalKb()))); + long target = reservoirTarget(snap); boolean ok = target > 0 && model.saveCmaTarget(target).ok(); if (ok) { var s2 = model.state(); @@ -1096,541 +1048,123 @@ private void enableCmaDirect(@NonNull HugePageModel.Snapshot snap) { }); } - /* ---- probe orchestration ---- */ - - /** Progress dialog handle for the probe worker thread. */ - private static final class ProbeUi { - @NonNull final androidx.appcompat.app.AlertDialog dialog; - @NonNull final TextView text; - - ProbeUi(@NonNull androidx.appcompat.app.AlertDialog dialog, @NonNull TextView text) { - this.dialog = dialog; - this.text = text; - } - } - - private static final int VERDICT_DENIED = 0; - private static final int VERDICT_ALLOWED = 1; - private static final int VERDICT_ABNORMAL = -1; - - /** max(RAM - 8 GiB, RAM x 0.4) in bytes; -1 when meminfo is unreadable. */ - private long probeNeedBytes(long memTotalKb) { - if (memTotalKb <= 0) return -1; - long total = memTotalKb * 1024; - return Math.max(total - PROBE_KEEP_BYTES, (long) (total * PROBE_MIN_RAM_FRACTION)); - } - - private static long pagesFor(long bytes) { - return (bytes + PAGE_SIZE - 1) / PAGE_SIZE; - } - - /** Kick off the probe worker; the switch stays under the flow's control. */ - private void startCmaProbe() { - new Thread(this::runCmaProbe, "hugepage-cma-probe").start(); - } - /** - * The consumability probe (worker thread). Steps, per the module docs: - * precondition {@code avail >= max(RAM-7G, 40% RAM)} (guided acquire, else - * save-and-reboot); then {@code echo avail > pool_want_with_cma}, - * {@code echo 0 > pool_want} (the freed blocks flip to the reservoir), run - * {@code balloon 1536} and judge from how much CmaFree the pressure consumed - * whether this vendor lets user apps allocate from CMA. {@code pool_want} is - * restored on every path. - * - *

    Only a pass is written to settings.prop ({@code cma_probe_result=1}) - - * an unreadable result asks the user, and enabling counts as a pass. A - * failure (or a decline) records nothing and clears any stale verdict, so - * the next launch can simply probe again. + * Arm a movable->CMA lever live only - nothing is persisted yet. On + * 6.1 the {@code restrict_cma_redirect} flag is side-effect-free, so try it + * first and fall back to the gfp hook; on 6.6/6.12 that same key also backs + * {@code cma_has_pcplist()}, so arm the narrower hook directly (and only try + * the flag as a last resort). Then build the reservoir toward a target so the + * user can watch it fill, and offer to save the lever for next boot. */ - private void runCmaProbe() { - var cancelled = new AtomicBoolean(false); - ProbeUi ui = null; - boolean raised = false; // pool_want_with_cma raised by us - boolean zeroed = false; // pool_want emptied by us - long prevWant = -1; - try { - // Balloon pressure would squeeze (or LMK-kill) running VMs. - if (runningVmMemMib() > 0) { - probeFail(null, getString(R.string.hugepage_cma_vms_running)); - return; - } - var snap = model.state(); - prevWant = snap.targetIdeal; - long needBytes = probeNeedBytes(model.memTotalKb()); - if (needBytes <= 0) { - probeFail(null, getString(R.string.hugepage_cma_probe_failed_generic)); - return; - } - long needPages = pagesFor(needBytes); - // 1. How big a reservoir to measure against. It is NOT bounded by - // the configured pool: the probe empties the pool into the - // reservoir (pool_want=0 -> the module's shrink flips every avail - // block to CMA, instantly), so the only ceiling is the module's - // RAM-derived pool_size_max. A cap below what we want makes the - // verdict shakier, not impossible: warn and let the user go on. - long cap = model.poolSizeMax(); - long target = Math.max(needPages, Math.max(prevWant, snap.wantWithCma)); - if (cap > 0) target = Math.min(target, cap); - long goal = Math.min(needPages, target); - if (goal < needPages && !probeAsk( - getString(R.string.hugepage_cma_small_reservoir_title), - getString(R.string.hugepage_cma_small_reservoir_msg, - SizeUtils.formatSize(goal * PAGE_SIZE), - SizeUtils.formatSize(needPages * PAGE_SIZE)), - getString(R.string.hugepage_cma_probe_anyway))) { - probeCancelled(null, false); - return; - } - ui = probeProgressShow(reservoirStage(snap.cmaPool, goal), cancelled); - if (ui == null) { - probeCancelled(null, false); - return; - } - // 2. Build the reservoir. Already there (a previous run persisted the - // target and the module assembled it at boot) -> measure directly, - // touching nothing. - boolean built = snap.cmaPool >= goal; - if (!built) { - // Raise the total first: pool_want=0 would otherwise soft-disable - // the pool and hand its pages back to the buddy allocator instead - // of flipping them into the reservoir. Never lower an existing - // bigger total - that demolishes reservoir the device is lending - // out right now. - if (snap.wantWithCma < target) { - var w = model.writeWantWithCma(target); - if (!w.ok()) { - probeDismiss(ui); - probeUnavailable(getString(R.string.hugepage_cma_unavailable_write, - w.detail != null ? w.detail : "?")); - return; - } - } - raised = true; - // Empty the pool: its avail blocks flip to CMA at once (the fast - // path - a from-scratch sweep would hit the fragmentation wall). - // Only the live knob is written, so a reboot restores pool_want - // even if this app dies before the restore below. - var shrink = model.writeWant(0); - if (!shrink.ok()) { - probeRollback(); - probeDismiss(ui); - probeFail(null, getString(R.string.hugepage_cma_probe_failed_generic)); - return; - } - zeroed = true; - Thread.sleep(3000); // let the flips land - model.acquire(3); // best-effort top-up toward the target - built = waitReservoir(ui, goal, cancelled); - } - if (cancelled.get()) { - model.stopAcquire(); - probeRestore(prevWant, zeroed); - probeRollback(); - probeCancelled(ui, true); - return; - } - // Whatever actually assembled is what the balloon is judged against. - long reservoirPages = model.state().cmaPool; - if (reservoirPages <= 0) { - probeRestore(prevWant, zeroed); - probeRollback(); - probeDismiss(ui); - probeUnavailable(getString(R.string.hugepage_cma_unavailable_reservoir)); - return; - } - if (!built) { - // The runtime sweep hit the fragmentation wall. This is not a - // different failure from "it can't be built" - the module builds - // the reservoir first at init, on the cleanest memory there is, - // so the very same target simply works after a reboot. Persist - // it and pick the probe back up then (see the pending prompt) - - // or measure right now against the smaller reservoir that did - // get built, accepting a shakier verdict. - probeDismiss(ui); - ui = null; - int choice = probeAskChoice( - getString(R.string.hugepage_cma_reservoir_short_title), - getString(R.string.hugepage_cma_reservoir_short_msg, - SizeUtils.formatSize(reservoirPages * PAGE_SIZE), - SizeUtils.formatSize(goal * PAGE_SIZE)), - getString(R.string.hugepage_cma_save_reboot), - getString(R.string.hugepage_cma_probe_anyway)); - if (choice != CHOICE_NEGATIVE) { - // "I'll reboot, then probe", or dismissed. Nothing is saved: - // the probe assembles the reservoir out of the pool itself, - // so a fresh boot - where memory is unfragmented - simply - // lets the same run succeed. Undo our live writes and go. - probeRestore(prevWant, zeroed); - probeRollback(); - if (choice == CHOICE_POSITIVE) - probeToast(getString(R.string.hugepage_cma_reboot_hint)); - probeEndUi(false); - return; - } - // Probe anyway: a fresh progress dialog for the pressure stage. - ui = probeProgressShow( - getString(R.string.hugepage_cma_probe_running_balloon), cancelled); - if (ui == null) { - probeRestore(prevWant, zeroed); - probeRollback(); - probeCancelled(null, false); - return; - } - } - // 3. Pressure + judgment, measured against the reservoir that exists. - probeStage(ui, getString(R.string.hugepage_cma_probe_running_balloon)); - var out = model.runBalloon(BALLOON_FLOOR_MB, BALLOON_TIMEOUT_S); - if (cancelled.get()) { - probeRestore(prevWant, zeroed); - probeRollback(); - probeCancelled(ui, true); - return; - } - int verdict = judgeBalloon(out, reservoirPages); - probeDismiss(ui); - ui = null; - // A pass enables straight away. Anything else - a denial, or numbers - // that match neither verdict - is put to the user, because a probe - // can be wrong (a small reservoir, a vendor that only lets some - // allocation classes in). Enabling either way counts as a pass and - // is recorded, so the switch stops probing from now on; declining - // records nothing and clears any stale verdict, leaving the probe - // available next launch. - if (verdict == VERDICT_ALLOWED) { - model.setCmaProbeAllowed(); - probeEnableWithSmallPool(target); - probeToast(getString(R.string.hugepage_cma_probe_ok, - SizeUtils.formatSize(PROBE_POOL_BYTES))); - probeEndUi(true); + private void tryCmaLever(@NonNull HugePageModel.Snapshot snap) { + cmaBusy = true; + runOnPool(() -> { + boolean is61 = model.kernelIs61(); + String lever = null; + if (is61 && flipFlagWorks()) lever = HugePageModel.LEVER_FLAG; + else if (model.setGfpHook(true).ok()) lever = HugePageModel.LEVER_HOOK; + else if (!is61 && flipFlagWorks()) lever = HugePageModel.LEVER_FLAG; + if (lever == null) { + runOnUiThread(() -> { + cmaBusy = false; + setCmaSwitch(false); + Toast.makeText(this, R.string.hugepage_cma_lever_failed, + LENGTH_SHORT).show(); + refreshStatus(); + }); return; } - boolean enable = verdict == VERDICT_DENIED - ? probeAsk(getString(R.string.hugepage_cma_probe_denied_title), - getString(R.string.hugepage_cma_probe_denied_result), - getString(R.string.hugepage_cma_probe_enable)) - : probeAskAbnormal(out, reservoirPages); - if (enable) { - model.setCmaProbeAllowed(); - probeEnableWithSmallPool(target); // also restores pool_want - probeToast(getString(R.string.hugepage_cma_enabled_pool, - SizeUtils.formatSize(PROBE_POOL_BYTES))); - probeEndUi(true); - } else { - probeRestore(prevWant, zeroed); - model.clearCmaProbeResult(); - model.saveCmaTarget(0); // demolishes the reservoir - if (zeroed) model.acquire(1); // refill the restored pool - probeEndUi(false); - } - } catch (InterruptedException e) { - probeRestore(prevWant, zeroed); - if (raised) probeRollback(); - probeCancelled(ui, ui != null); - } catch (Exception e) { - // Never leave the flow lock stuck: undo our writes and surface the - // error instead of a wedged switch. - Log.w(TAG, "CMA probe failed", e); - probeRestore(prevWant, zeroed); - if (raised) probeRollback(); - probeDismiss(ui); - probeFail(null, getString(R.string.hugepage_cma_probe_failed_generic)); - } - } - - /** Put {@code pool_want} back if the probe emptied it (live knob only). */ - private void probeRestore(long prevWant, boolean zeroed) { - if (zeroed && prevWant >= 0) model.writeWant(prevWant); - } - - /** - * The reboot half of the probe: the reservoir target survived a reboot with - * no verdict recorded, so the module has now built it on clean memory and - * the measurement can finally run. Asked once per visit; declining leaves - * the reservoir in place (it is still lent to apps) and the switch on. - */ - private void promptPendingProbe() { - if (probePromptShown || cmaBusy || isFinishing() || isDestroyed()) return; - probePromptShown = true; - new MaterialAlertDialogBuilder(this) - .setTitle(R.string.hugepage_cma_probe_pending_title) - .setMessage(R.string.hugepage_cma_probe_pending_msg) - .setPositiveButton(R.string.hugepage_cma_probe_continue, (d, w) -> { - cmaBusy = true; - startCmaProbe(); - }) - .setNegativeButton(android.R.string.cancel, null) - .show(); - } - - /** Undo the probe's only write: the raised total demolishes its reservoir. */ - private void probeRollback() { - model.writeWantWithCma(0); - } - - @NonNull - private String reservoirStage(long got, long goal) { - return getString(R.string.hugepage_cma_building, - SizeUtils.formatSize(got * PAGE_SIZE), SizeUtils.formatSize(goal * PAGE_SIZE)); - } - - /** - * The end state of an enabled probe: a {@value #PROBE_POOL_BYTES}-byte pool - * and the with-CMA total the probe assembled, persisted in one settings.prop - * rewrite so the next boot comes up the same way. This is the probe's first - * and only {@code pool_want} write: shrinking the pool hands its pages to - * the reservoir (the module's shrink path), and the v3 acquire then stages - * 512 MB back in and tops the reservoir up toward the total. - */ - private void probeEnableWithSmallPool(long total) { - model.saveTargets(PROBE_POOL_PAGES, total); - // v3: only the mode-2/3 sweep runs the reservoir-building Phase R. - model.acquire(3); + buildReservoirAndPromptSave(snap, lever); + }); } /** - * Poll the reservoir toward {@code goal} pages, narrating progress. Returns - * true once {@code pool_cma} reaches it; false on stop/timeout/cancel. + * Reservoir-only: build the reservoir without arming any lever, so + * there is no crash risk. Useful on a device where the vendor kernel already + * lets apps consume CMA even though it reads as not-allowed; elsewhere the + * reserve still serves VMs via stage-in. Offers the same save step (which + * persists the target but no lever). */ - private boolean waitReservoir(@NonNull ProbeUi ui, long goal, - @NonNull AtomicBoolean cancelled) - throws InterruptedException { - for (int i = 0; i < 1800; i++) { // 30 min hard bound - if (cancelled.get()) return false; - var s = model.state(); - probeStage(ui, reservoirStage(s.cmaPool, goal)); - if (s.cmaPool >= goal) return true; - // Give the worker a few seconds to raise acquire_active before - // treating "not acquiring" as done-short. - if (!s.acquiring && i > 5) return false; - Thread.sleep(1000); - } - return false; + private void enableCmaReservoirOnly(@NonNull HugePageModel.Snapshot snap) { + cmaBusy = true; + runOnPool(() -> buildReservoirAndPromptSave(snap, null)); } /** - * Judge the balloon output against the reservoir that was built: pressure - * that consumed at least half of it means apps allocate from CMA; a tenth - * or less means they can't; anything between (or unparsable output) is - * unreadable and goes to the user. + * Build the reservoir toward a target with a live (non-persisted) write so + * the user can watch it fill, then offer to save. {@code lever} is the lever + * the caller armed, or {@code null} for the reservoir-only path. Runs on the + * pool thread. */ - private int judgeBalloon(@Nullable Map out, long reservoirPages) { - if (out == null) return VERDICT_ABNORMAL; - long diffKb; - long heldMb; - try { - diffKb = Long.parseLong(out.getOrDefault("cma_diff_kb", "").trim()); - heldMb = Long.parseLong(out.getOrDefault("held_mb", "").trim()); - } catch (NumberFormatException e) { - return VERDICT_ABNORMAL; + private void buildReservoirAndPromptSave(@NonNull HugePageModel.Snapshot snap, + @Nullable String lever) { + long target = reservoirTarget(snap); + if (target > 0) { + model.writeWantWithCma(target); + var s2 = model.state(); + if (s2.loaded && s2.deficit > 0) model.acquire(3); } - long reservoirKb = reservoirPages * (PAGE_SIZE / 1024); - if (reservoirKb <= 0 || heldMb <= 0) return VERDICT_ABNORMAL; - if (diffKb >= reservoirKb / 2) return VERDICT_ALLOWED; - if (diffKb <= reservoirKb / 10) return VERDICT_DENIED; - return VERDICT_ABNORMAL; - } - - /* ---- probe worker <-> UI plumbing (all blocking helpers) ---- */ - - /** Blocking two-choice dialog; false on cancel/back/finish. */ - private boolean probeAsk(@NonNull String title, @NonNull String message, - @NonNull String positive) throws InterruptedException { - return probeAskChoice(title, message, positive, - getString(android.R.string.cancel)) == CHOICE_POSITIVE; - } - - /** {@link #probeAskChoice} outcomes; {@code CHOICE_NONE} = back/dismiss/gone. */ - private static final int CHOICE_NONE = 0; - private static final int CHOICE_POSITIVE = 1; - private static final int CHOICE_NEGATIVE = 2; - - /** - * Blocking dialog offering two named actions, with back/outside-tap as a - * third "neither" outcome. Both buttons are real choices - which is why - * neither is labelled Cancel by callers that need three ways out. - */ - private int probeAskChoice(@NonNull String title, @NonNull String message, - @NonNull String positive, @NonNull String negative) - throws InterruptedException { - var choice = new AtomicInteger(CHOICE_NONE); - var latch = new CountDownLatch(1); runOnUiThread(() -> { - // isDestroyed covers rotation teardown (isFinishing stays false); - // the catch covers a window torn down mid-post - either way the - // latch MUST be counted or the worker blocks forever. - if (isFinishing() || isDestroyed()) { - latch.countDown(); - return; - } - try { - new MaterialAlertDialogBuilder(this) - .setTitle(title) - .setMessage(message) - .setPositiveButton(positive, (d, w) -> choice.set(CHOICE_POSITIVE)) - .setNegativeButton(negative, (d, w) -> choice.set(CHOICE_NEGATIVE)) - .setOnDismissListener(d -> latch.countDown()) - .show(); - } catch (Exception e) { - latch.countDown(); - } - }); - latch.await(); - return choice.get(); - } - - /** The "result unreadable - enable anyway?" dialog, with the raw numbers. */ - private boolean probeAskAbnormal(@Nullable Map out, long reservoirPages) - throws InterruptedException { - long diffKb = 0; - long heldMb = 0; - String stop = "?"; - if (out != null) { - try { - diffKb = Long.parseLong(out.getOrDefault("cma_diff_kb", "0").trim()); - } catch (NumberFormatException ignored) { - } - try { - heldMb = Long.parseLong(out.getOrDefault("held_mb", "0").trim()); - } catch (NumberFormatException ignored) { - } - stop = out.getOrDefault("stop_reason", "?"); - } - var choice = new AtomicInteger(0); - var latch = new CountDownLatch(1); - long fDiffKb = diffKb; - long fHeldMb = heldMb; - String fStop = stop; - runOnUiThread(() -> { - if (isFinishing() || isDestroyed()) { - latch.countDown(); + cmaInputLoaded = false; + refreshStatus(); + if (isFinishing()) { + cmaBusy = false; return; } - try { - new MaterialAlertDialogBuilder(this) - .setTitle(R.string.hugepage_cma_probe_abnormal_title) - .setMessage(getString(R.string.hugepage_cma_probe_abnormal_msg, - SizeUtils.formatSize(fDiffKb * 1024), - SizeUtils.formatSize(reservoirPages * PAGE_SIZE), - SizeUtils.formatSize(fHeldMb * 1024 * 1024), - fStop)) - .setPositiveButton(R.string.hugepage_cma_probe_enable, - (d, w) -> choice.set(1)) - .setNegativeButton(R.string.hugepage_cma_probe_keep_off, null) - .setOnDismissListener(d -> latch.countDown()) - .show(); - } catch (Exception e) { - latch.countDown(); - } - }); - latch.await(); - return choice.get() == 1; - } - - /** Show the cancellable progress dialog; null when the activity is gone. */ - @Nullable - private ProbeUi probeProgressShow(@NonNull String initial, - @NonNull AtomicBoolean cancelled) - throws InterruptedException { - var holder = new java.util.concurrent.atomic.AtomicReference(); - var latch = new CountDownLatch(1); - runOnUiThread(() -> { - try { - if (isFinishing() || isDestroyed()) return; - float density = getResources().getDisplayMetrics().density; - var text = new TextView(this); - text.setText(initial); - var box = new LinearLayout(this); - box.setOrientation(LinearLayout.HORIZONTAL); - box.setGravity(android.view.Gravity.CENTER_VERTICAL); - int pad = Math.round(24 * density); - box.setPaddingRelative(pad, Math.round(16 * density), pad, 0); - var spinner = new ProgressBar(this); - box.addView(spinner, Math.round(32 * density), Math.round(32 * density)); - var lp = new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, - LinearLayout.LayoutParams.WRAP_CONTENT); - lp.setMarginStart(Math.round(16 * density)); - box.addView(text, lp); - var dialog = new MaterialAlertDialogBuilder(this) - .setTitle(R.string.hugepage_enable_cma) - .setView(box) - .setCancelable(false) - .setNegativeButton(android.R.string.cancel, null) - .create(); - dialog.show(); - // Cancel requests cooperative interruption; the worker decides - // when it is safe to stop, so the button must not dismiss the - // dialog. A running balloon ignores flags, so also kill it - - // its run() then returns quickly and the worker sees the flag. - var btn = dialog.getButton(android.content.DialogInterface.BUTTON_NEGATIVE); - if (btn != null) btn.setOnClickListener(v -> { - cancelled.set(true); - v.setEnabled(false); - runOnPool(() -> runList("pkill", "-f", - "gh-hugepage-reserve/balloon")); - }); - holder.set(new ProbeUi(dialog, text)); - } catch (Exception ignored) { - // window torn down mid-post: holder stays null = "activity gone" - } finally { - latch.countDown(); - } + promptSaveLever(lever, target); }); - latch.await(); - return holder.get(); - } - - private void probeStage(@NonNull ProbeUi ui, @NonNull String msg) { - runOnUiThread(() -> ui.text.setText(msg)); } - private void probeDismiss(@Nullable ProbeUi ui) { - if (ui != null) runOnUiThread(ui.dialog::dismiss); + /** Flip the restrict flag on and confirm the kernel actually opened it. */ + private boolean flipFlagWorks() { + return model.setRestrictFlip(true).ok() && model.readRestrictState() == 1; } - private void probeToast(@NonNull String msg) { - runOnUiThread(() -> Toast.makeText(this, msg, Toast.LENGTH_LONG).show()); + /** With-CMA target to build: the last saved total, else the pool size. */ + private long reservoirTarget(@NonNull HugePageModel.Snapshot snap) { + return Math.max(model.lastCmaTargetPages(), + Math.max(snap.targetIdeal, snap.built)); } - /** Wind the flow down: dismiss, switch off. Callers own the rollback. */ - private void probeCancelled(@Nullable ProbeUi ui, boolean toast) { - probeDismiss(ui); - if (toast) probeToast(getString(R.string.hugepage_cma_probe_cancelled)); - probeEndUi(false); + /** + * Step 2: the live setting didn't crash the phone. Offer to persist it so it + * re-applies at the next boot (written into Magisk's settings.prop). + * Declining keeps it running now but leaves the next boot clean - the safety + * net, in case it destabilises the device after all. {@code lever} is null on + * the reservoir-only path (only the target is persisted). + */ + private void promptSaveLever(@Nullable String lever, long target) { + new MaterialAlertDialogBuilder(this) + .setTitle(R.string.hugepage_cma_save_title) + .setMessage(R.string.hugepage_cma_save_msg) + .setPositiveButton(R.string.hugepage_cma_save_yes, + (d, w) -> finishCmaEnable(lever, target, true)) + .setNegativeButton(R.string.hugepage_cma_save_no, + (d, w) -> finishCmaEnable(lever, target, false)) + .setOnCancelListener(d -> finishCmaEnable(lever, target, false)) + .show(); } - /** Failure with a message dialog (or toast when {@code title} is null). */ - private void probeFail(@Nullable String title, @NonNull String message) { - runOnUiThread(() -> { - cmaBusy = false; - setCmaSwitch(false); - if (isFinishing()) return; - if (title == null) { - Toast.makeText(this, message, Toast.LENGTH_LONG).show(); - } else { - new MaterialAlertDialogBuilder(this) - .setTitle(title) - .setMessage(message) - .setPositiveButton(android.R.string.ok, null) - .show(); + /** + * Settle an enabled reservoir: switch on and reseed the size input. When + * {@code save}, persist the with-CMA target and the lever choice so the next + * boot comes up the same way (a null lever clears the key = reservoir-only); + * otherwise everything stays live-only. + */ + private void finishCmaEnable(@Nullable String lever, long target, boolean save) { + runOnPool(() -> { + if (save) { + if (lever != null) model.saveCmaLever(lever); + else model.clearCmaLever(); + if (target > 0) model.saveCmaTarget(target); } - refreshStatus(); - }); - } - - private void probeUnavailable(@NonNull String detail) { - probeFail(getString(R.string.hugepage_cma_unavailable_title), detail); - } - - /** Release the flow lock and settle the switch to the final state. */ - private void probeEndUi(boolean enabled) { - runOnUiThread(() -> { - cmaBusy = false; - setCmaSwitch(enabled); - cmaInputLoaded = false; // reseed the with-CMA total field - loadPoolSize(); // a passing probe pins pool_want to 512 MB - refreshStatus(); + runOnUiThread(() -> { + cmaBusy = false; + setCmaSwitch(true); + Toast.makeText(this, R.string.hugepage_cma_enabled, LENGTH_SHORT).show(); + cmaInputLoaded = false; + refreshStatus(); + }); }); } diff --git a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageModel.java b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageModel.java index 350283e91..aef1fb615 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageModel.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageModel.java @@ -440,53 +440,122 @@ Result stopAcquire() { } /* ================================================================== */ - /* v10 CMA reservoir + consumability probe */ + /* v11 movable->CMA levers + CMA reservoir persistence */ /* ================================================================== */ + /** settings.prop values recording which movable->CMA lever the user saved. */ + static final String LEVER_HOOK = "hook"; + static final String LEVER_FLAG = "flag"; + private static final String CMA_LEVER_KEY = "cma_movable_lever"; + + private static final String MTC_VENDER = "moveable_to_cma_vender_already_allowed"; + private static final String MTC_RESTRICT = "moveable_to_cma_restrict_cma_redirect_disabled"; + private static final String MTC_GFP_HOOK = "moveable_to_cma_gfp_cma_hook"; + /** - * The app-side probe verdict recorded in settings.prop as - * {@code cma_probe_result}: {@code 1} = apps can consume the reservoir, - * {@code 0} = they can't (the boot script then keeps the whole CMA side - * cold), {@code null} = the probe never ran. + * Whether the running kernel is 6.1. On 6.1 the {@code restrict_cma_redirect} + * static key is side-effect-free, so the enable flow prefers flipping it (a + * clean global switch); on 6.6/6.12 the same key also backs + * {@code cma_has_pcplist()}, so the narrower gfp hook is used instead. Fails + * safe to {@code false} (the hook path) when {@code uname} is unreadable. */ - @Nullable - Integer cmaProbeResult() { - var v = parseProp(safeRead(SETTINGS_PROP)).get("cma_probe_result"); - if (v == null) return null; + boolean kernelIs61() { try { - return Integer.parseInt(v.trim()); - } catch (NumberFormatException e) { - return null; + var r = runList("uname", "-r").getOutString().trim(); + return r.equals("6.1") || r.startsWith("6.1."); + } catch (Exception e) { + return false; } } /** - * Record a passed probe (see {@link #cmaProbeResult}). Only success - * is ever persisted: a failed or inconclusive probe leaves no verdict, so - * the next app launch can simply probe again. + * Read {@code moveable_to_cma_vender_already_allowed}: {@code 1} = the + * vendor kernel already redirects every movable allocation into CMA, so the + * reservoir is consumable without touching either lever; {@code 0} = it does + * not; {@code -1} = the param is absent (pre-v11 module, no lever support). + */ + int mtcVenderAllowed() { + return readIntParam(MTC_VENDER, -1); + } + + /** + * The "flag" lever: flip the kernel {@code restrict_cma_redirect} static key + * (write 1 = open movable->CMA globally). Live only - not persisted here. + */ + @NonNull + Result setRestrictFlip(boolean on) { + var t = writeKnob(MTC_RESTRICT, on ? "1" : "0"); + return t.ok() ? Result.ok(MTC_RESTRICT) : Result.failed(MTC_RESTRICT, t.error); + } + + /** Read the flag state: 1 = redirect open, 0 = blocked, -1 = unresolvable. */ + int readRestrictState() { + return readIntParam(MTC_RESTRICT, -1); + } + + /** + * The "hook" lever: arm the {@code __GFP_CMA} bypass hook (write 1 = let page + * cache / mTHP anon consume the reservoir). Live only - not persisted here. */ @NonNull - Result setCmaProbeAllowed() { + Result setGfpHook(boolean on) { + var t = writeKnob(MTC_GFP_HOOK, on ? "1" : "0"); + return t.ok() ? Result.ok(MTC_GFP_HOOK) : Result.failed(MTC_GFP_HOOK, t.error); + } + + /** Read the gfp hook arm state: 1 = armed, 0 = disarmed, -1 = absent. */ + int readGfpHook() { + return readIntParam(MTC_GFP_HOOK, -1); + } + + /** + * Persist the chosen movable->CMA lever ({@link #LEVER_HOOK} / + * {@link #LEVER_FLAG}) under an app-owned settings.prop key, so a future + * boot script can re-apply it as an insmod param. The shipped load.sh does + * not read it yet, so this only records intent - deliberate: the lever is + * applied live and only saved once it has proven it doesn't crash the boot. + */ + @NonNull + Result saveCmaLever(@NonNull String lever) { var changes = new LinkedHashMap(); - changes.put("cma_probe_result", "1"); + changes.put(CMA_LEVER_KEY, lever); var t = updateSettings(changes); return t.ok() ? Result.ok("settings") : Result.failed("settings", t.error); } - /** - * Drop any recorded verdict. Beyond "forget a failure", this un-sticks a - * legacy {@code cma_probe_result=0}: the boot script hands the module -1 - * preflight values while that key is 0, which kills the CMA side for the - * whole boot and makes a re-probe impossible until it is gone. - */ + /** Forget the persisted lever (CMA switched off, or the user declined save). */ @NonNull - Result clearCmaProbeResult() { + Result clearCmaLever() { var changes = new LinkedHashMap(); - changes.put("cma_probe_result", null); // null value = remove the key + changes.put(CMA_LEVER_KEY, null); // null value = remove the key var t = updateSettings(changes); return t.ok() ? Result.ok("settings") : Result.failed("settings", t.error); } + /** + * Drop a stale {@code cma_probe_result} left by an older app version. The + * shipped boot script still cold-starts the whole CMA side on + * {@code cma_probe_result=0}, so a leftover denial from the removed probe + * would keep v11's reservoir off; remove it on sight. No-op when absent. + */ + void clearLegacyProbeKey() { + if (!parseProp(safeRead(SETTINGS_PROP)).containsKey("cma_probe_result")) return; + var changes = new LinkedHashMap(); + changes.put("cma_probe_result", null); + updateSettings(changes); + } + + /** Read an integer sysfs param, or {@code def} when absent/unparseable. */ + private int readIntParam(@NonNull String name, int def) { + var v = safeRead(pathJoin(SYSFS_PARAMS, name)).trim(); + if (v.isEmpty()) return def; + try { + return Integer.parseInt(v); + } catch (NumberFormatException e) { + return def; + } + } + /** * The last non-zero with-CMA total (pages) the user ran with, kept under an * app-owned settings.prop key so switching CMA off (which must persist @@ -553,7 +622,12 @@ private static Map cmaTargetChanges(long pages) { return changes; } - /** Live {@code pool_want_with_cma} write only - the probe's first step. */ + /** + * Live {@code pool_want_with_cma} write only (no settings.prop persist), so + * the reservoir target set here is undone by a reboot. The enable flow uses + * it to build the reservoir at runtime before the user decides whether to + * save the movable->CMA lever. + */ @NonNull Result writeWantWithCma(long pages) { var t = writeKnob("pool_want_with_cma", Long.toString(pages)); @@ -561,73 +635,6 @@ Result writeWantWithCma(long pages) { : Result.failed("pool_want_with_cma", t.error); } - /** - * Live {@code pool_want} write only (no settings.prop persist), so a value - * written here is undone by a reboot. The probe uses it to empty the pool - * into the reservoir and to put it back afterwards. - */ - @NonNull - Result writeWant(long pages) { - var t = writeKnob("pool_want", Long.toString(pages)); - return t.ok() ? Result.ok("pool_want") : Result.failed("pool_want", t.error); - } - - /** MemTotal from /proc/meminfo in KiB, or -1 if unreadable. */ - long memTotalKb() { - return meminfoKb("MemTotal"); - } - - /** - * The module's RAM-derived cap on the targets (pages), read-only: - * {@code min(ram - min(ram/2, 6G), 24G)}. Both {@code pool_want} and - * {@code pool_want_with_cma} are clamped to it, so it bounds how much - * reservoir can sit on top of a given pool. -1 when unreadable (pre-v7). - */ - long poolSizeMax() { - try { - var v = shellReadFile(pathJoin(SYSFS_PARAMS, "pool_size_max")).trim(); - if (!v.isEmpty()) return Long.parseLong(v); - } catch (Exception ignored) { - } - return -1; - } - - private long meminfoKb(@NonNull String key) { - var prefix = fmt("%s:", key); - for (var line : safeRead("/proc/meminfo").split("\n")) { - if (!line.startsWith(prefix)) continue; - var digits = NON_DIGITS.matcher(line).replaceAll(""); - if (!digits.isEmpty()) try { - return Long.parseLong(digits); - } catch (NumberFormatException ignored) { - } - } - return -1; - } - - /** - * Run the module-shipped {@code balloon} pressure tool (see tools/balloon.c): - * it anon-balloons until MemAvailable drops under {@code floorMb} and prints - * {@code cma_before_kb / cma_after_kb / cma_diff_kb / held_mb / stop_reason}. - * Supervised with {@code timeout} (falling back to unsupervised where the - * command is missing) because on a kernel that does NOT let anon consume CMA - * the floor can fire very late or never. Returns the parsed key=value output, - * or {@code null} when the tool is absent, was killed, or printed nothing - * usable - the caller treats that as an unreadable verdict. - */ - @Nullable - Map runBalloon(long floorMb, long timeoutSec) { - var bin = pathJoin(MAGISK_BASE, "balloon"); - if (!existsSticky(bin)) return null; - var r = run("chmod 755 %s && timeout %d %s %d", - escapedString(bin), timeoutSec, escapedString(bin), floorMb); - if (r.getCode() == 127) // no timeout applet on this ROM - r = run("%s %d", escapedString(bin), floorMb); - if (!r.isSuccess()) return null; - var map = parseProp(r.getOutString()); - return map.containsKey("cma_diff_kb") ? map : null; - } - /* ================================================================== */ /* Ladder plumbing (internal) */ /* ================================================================== */ @@ -816,10 +823,10 @@ private static Try writeSettings(long pages) { /** * Read-modify-write settings.prop: apply {@code changes} (a null value * removes that key) and keep every other key (the file also carries - * app-owned CMA state - {@code pool_want_with_cma}, {@code cma_probe_result} + * app-owned CMA state - {@code pool_want_with_cma}, {@code cma_movable_lever} * - that a blind rewrite would wipe). The boot script {@code source}s the * file, so lines stay plain {@code key=value}. Locked so concurrent writers - * (the probe worker vs a pool-size save) can't interleave their read/write + * (a lever/CMA save vs a pool-size save) can't interleave their read/write * pairs and drop each other's keys. */ private static Try updateSettings(@NonNull Map changes) { @@ -830,7 +837,7 @@ private static Try updateSettings(@NonNull Map chang } catch (Exception e) { // A missing file legitimately starts empty; an EXISTING file that // failed to read must abort - rewriting from an empty map would - // silently drop every other persisted key (probe verdict, CMA + // silently drop every other persisted key (lever choice, CMA // targets) on a transient root hiccup. if (existsSticky(SETTINGS_PROP)) return Try.fail(Only.DEFAULT, "settings.prop: read failed"); diff --git a/app/src/main/res/layout/activity_hugepage.xml b/app/src/main/res/layout/activity_hugepage.xml index c8191a597..98a6e5c87 100644 --- a/app/src/main/res/layout/activity_hugepage.xml +++ b/app/src/main/res/layout/activity_hugepage.xml @@ -113,18 +113,18 @@ - + + android:orientation="vertical"> + app:ti_unit="GiB" /> 模块未加载或不支持 CMA(需 v10+) CMA 不可用 本次开机模块已停用 CMA 功能(内核预检/符号解析/首块验证未通过)。 - 先前探测曾判定 CMA 不可用,该记录会让模块整个开机期间关闭 CMA。记录已清除,请重启后再次开启 CMA 以重新探测。 - 写入 pool_want_with_cma 被拒:%1$s - 无法组出任何 CMA 料场(headroom floor 拒绝,或区块翻转失败)。 - 需要先进行 CMA 探测 - 首次使用前需探测该厂商内核是否允许 App 使用 CMA 内存。\n\n探测会暂时清空池、把它整块转成 CMA 料场(目标约 %1$s),用 balloon 工具施加内存压力,再按 CMA 消耗量判读。结束后会还原池,进行前所有 VM 需先关闭。\n\n探测通过后,池大小会设为 %2$s,其余保留为料场借给 App,直到 VM 需要时才取回。 - 开始探测 - 直接进行探测 - 完成探测 - 料场会偏小 - 该模块总共只能监护 %1$s,少于探测想要的 %2$s,判读结果可能不准确。仍要探测吗? - 尚未完成的 CMA 探测 - 已建立 CMA 料场但还没判读。要现在执行 balloon 测量,确认 App 是否真的用得到吗? - 建立 CMA 料场中\u2026 %1$s / %2$s - 料场未达目标 - 只组出 %1$s / %2$s,运行期的 sweep 撞到碎片墙。刚开机时内存没有碎片,同一个流程就会成功;可以重启后再次开启 CMA。或者直接用已组出的 %1$s 进行判读(可能较不准确)。 - CMA 已标记为不可用 - 先前探测结果:该设备的 App 无法使用 CMA,启用只会浪费保留池。要重新探测吗? - 重新探测 - 请先关闭所有运行中的 VM 再探测 - 我重启后再探测 - 未变更任何设置。请重启后再次开启 CMA:刚开机时内存没有碎片,料场就组得起来。 - 正在施加内存压力(balloon)\u2026 可能需要数分钟 - 探测通过:App 可使用 CMA。已启用,池大小设为 %1$s,其余作为料场借出。 - 探测结果:该设备的 App 无法使用 CMA,启用只会浪费保留池。仍要启用吗?您的选择会被记录,之后不再探测。 - 判读结果异常 - 压力共消耗 CMA %1$s / 池 %2$s(balloon 持有 %3$s,停止原因:%4$s),无法明确判读。仍要启用 CMA 吗? - 启用 - 保持关闭 - 已取消探测 - CMA 探测无法运行 + 如何启用 CMA + 该厂商内核默认不让 App 使用 CMA 保留区,请选择启用方式:\n\n• 模块 CMA:只建立保留区,不改动内核,没有崩溃风险。只有在该内核本来就允许时 App 才用得到;无论如何保留区仍能服务 VM。\n\n• 移除 CMA 限制:额外解除内核限制,让任何 App 都能使用保留区,但在部分设备上这可能导致手机崩溃并重启。会先应用但「不」保存:成功的话可再选择保存让每次开机都应用,崩溃的话直接重启即可,因为没有保存。 + 移除 CMA 限制 + 模块 CMA + 移除 CMA 限制失败:该内核没有可用的开关或挂钩。 + 每次开机都应用? + 目前运行正常。要保存让它每次开机都自动应用吗?\n\n若不保存,它只会维持到下次重启为止 —— 作为安全网,以防它之后才导致设备不稳。 + 保存 + 仅此一次 CMA 已启用 - CMA 已启用,池大小设为 %1$s,其余作为料场借出。 CMA 已停用 写入 CMA 目标失败 CMA diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 5300dd504..288034e21 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -1001,38 +1001,16 @@ 模組未載入或不支援 CMA(需 v10+) CMA 不可用 本次開機模組已停用 CMA 功能(kernel 預檢/符號解析/首塊驗證未通過)。 - 先前探測曾判定 CMA 不可用,該紀錄會讓模組整個開機期間關閉 CMA。紀錄已清除,請重新開機後再次開啟 CMA 以重新探測。 - 寫入 pool_want_with_cma 被拒:%1$s - 無法組出任何 CMA 料場(headroom floor 拒絕,或區塊翻轉失敗)。 - 需要先進行 CMA 探測 - 首次使用前需探測此廠商核心是否允許 App 使用 CMA 記憶體。\n\n探測會暫時清空池、把它整塊轉成 CMA 料場(目標約 %1$s),用 balloon 工具施加記憶體壓力,再依 CMA 消耗量判讀。結束後會還原池,進行前所有 VM 需先關閉。\n\n探測通過後,池大小會設為 %2$s,其餘保留為料場借給 App,直到 VM 需要時才取回。 - 開始探測 - 直接進行探測 - 完成探測 - 料場會偏小 - 此模組總共只能監護 %1$s,少於探測想要的 %2$s,判讀結果可能不準確。仍要探測嗎? - 尚未完成的 CMA 探測 - 已建立 CMA 料場但還沒判讀。要現在執行 balloon 量測,確認 App 是否真的用得到嗎? - 建立 CMA 料場中\u2026 %1$s / %2$s - 料場未達目標 - 只組出 %1$s / %2$s,執行期的 sweep 撞到碎片牆。剛開機時記憶體沒有碎片,同一個流程就會成功;可以重新開機後再次開啟 CMA。或者直接用已組出的 %1$s 進行判讀(可能較不準確)。 - CMA 已標記為不可用 - 先前探測結果:此裝置的 App 無法使用 CMA,啟用只會浪費保留池。要重新探測嗎? - 重新探測 - 請先關閉所有執行中的 VM 再探測 - 我重開機後再探測 - 未變更任何設定。請重新開機後再次開啟 CMA:剛開機時記憶體沒有碎片,料場就組得起來。 - 正在施加記憶體壓力(balloon)\u2026 可能需要數分鐘 - 探測通過:App 可使用 CMA。已啟用,池大小設為 %1$s,其餘作為料場借出。 - 探測結果:此裝置的 App 無法使用 CMA,啟用只會浪費保留池。仍要啟用嗎?您的選擇會被記錄,之後不再探測。 - 判讀結果異常 - 壓力共消耗 CMA %1$s / 料場 %2$s(balloon 持有 %3$s,停止原因:%4$s),無法明確判讀。仍要啟用 CMA 嗎? - 啟用 - 維持關閉 - 已取消探測 - CMA 探測無法執行 + 如何啟用 CMA + 此廠商核心預設不讓 App 使用 CMA 保留區,請選擇啟用方式:\n\n• 模組 CMA:只建立保留區,不改動核心,沒有崩潰風險。只有在此核心本來就允許時 App 才用得到;無論如何保留區仍能服務 VM。\n\n• 移除 CMA 限制:額外解除核心限制,讓任何 App 都能使用保留區,但在部分裝置上這可能造成手機崩潰並重新開機。會先套用但「不」存檔:成功的話可再選擇存檔讓每次開機都套用,崩潰的話直接重新開機即可,因為沒有存檔。 + 移除 CMA 限制 + 模組 CMA + 移除 CMA 限制失敗:此核心沒有可用的開關或掛鉤。 + 每次開機都套用? + 目前運作正常。要存檔讓它每次開機都自動套用嗎?\n\n若不存檔,它只會維持到下次重新開機為止 —— 作為安全網,以防它之後才造成裝置不穩。 + 存檔 + 僅此一次 CMA 已啟用 - CMA 已啟用,池大小設為 %1$s,其餘作為料場借出。 CMA 已停用 寫入 CMA 目標失敗 CMA diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 59d01665c..165aa357a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1038,38 +1038,16 @@ Module not loaded or has no CMA support (v10+) CMA unavailable The module disabled its CMA side this boot (kernel preflight / symbol resolution / first-block verification failed). - A previous probe recorded CMA as unusable, which keeps the module\'s CMA side off for the whole boot. That record has been cleared - reboot, then enable CMA again to re-probe. - Writing pool_want_with_cma was rejected: %1$s - No CMA reservoir could be assembled (the headroom floor refused it, or block flipping failed). - CMA probe required - Before first use, a probe must verify this vendor kernel lets apps allocate from CMA.\n\nIt temporarily empties the pool into a CMA reservoir (aiming for %1$s), applies memory pressure with the balloon tool, then judges from how much CMA got consumed. The pool is restored afterwards, and all VMs must be stopped first.\n\nOn success the pool is set to %2$s and the rest is kept as reservoir, lent to apps until a VM needs it. - Start probe - Probe anyway - Finish probe - Reservoir will be small - This module can only guard %1$s in total, less than the %2$s the probe wants to measure against, so the verdict may be unreliable. Probe anyway? - Unfinished CMA probe - A CMA reservoir was set up but never measured. Run the balloon measurement now to decide whether apps can really use it? - Building the CMA reservoir\u2026 %1$s / %2$s - Reservoir came up short - Assembled %1$s of %2$s - the runtime sweep hit the fragmentation wall. On a fresh boot memory is unfragmented, so the same run succeeds; reboot and turn CMA on again. Or measure right now against the %1$s that did assemble (the verdict may be less reliable). - CMA marked unusable - A previous probe found apps cannot use CMA on this device, so enabling it would only waste the reserve. Run the probe again? - Re-probe - Stop all running VMs before probing - I\'ll reboot, then probe - Nothing was changed. Reboot, then turn CMA on again - on a fresh boot memory is unfragmented and the reservoir assembles. - Applying memory pressure (balloon)\u2026 this can take several minutes - Probe passed: apps can use CMA. CMA enabled and the pool set to %1$s - the rest is lent out as reservoir. - Probe result: apps could not use CMA on this device, so enabling it would only waste the reserve. Enable anyway? Your choice is recorded and the probe will not run again. - Probe result unreadable - Pressure consumed %1$s of the %2$s reservoir (balloon held %3$s, stopped: %4$s). This matches neither verdict. Enable CMA anyway? - Enable - Keep off - Probe cancelled - CMA probe could not run + How to enable CMA + This vendor kernel does not let apps use the CMA reserve on its own, so choose how to enable it:\n\n- Module CMA: builds the reserve without changing the kernel, so there is no crash risk. Apps can use it only where the vendor kernel already allows it; either way the reserve still serves VMs.\n\n- Remove CMA Restriction: also lifts the kernel restriction so any app can use the reserve, but on some devices this can crash the phone and force a reboot. It is applied WITHOUT saving first; if it works you can then save it for every boot, and if it crashes just reboot - nothing was saved. + Remove CMA Restriction + Module CMA + Remove CMA Restriction failed: this kernel has no usable switch or hook. + Apply on every boot? + It is working now. Save this so it comes back automatically at every boot?\n\nIf you don\'t save, it stays active only until the next reboot - a safety net in case it destabilises the device. + Save + Just this time CMA enabled - CMA enabled and the pool set to %1$s - the rest is lent out as reservoir. CMA disabled Failed to write the CMA target CMA From 403c69247ebd109b1bf4cd019116d0975ab515f2 Mon Sep 17 00:00:00 2001 From: BigfootACA Date: Fri, 10 Jul 2026 15:57:49 +0800 Subject: [PATCH 121/182] libcompat: move missing setTransactionCodeToFunctionNameMap to a15 Signed-off-by: BigfootACA --- app/src/main/cpp/compat/a14.cpp | 6 ------ app/src/main/cpp/compat/a15.cpp | 6 ++++++ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/main/cpp/compat/a14.cpp b/app/src/main/cpp/compat/a14.cpp index 84a8775b3..5669679ad 100644 --- a/app/src/main/cpp/compat/a14.cpp +++ b/app/src/main/cpp/compat/a14.cpp @@ -1,5 +1,4 @@ #include "a15.cpp" -#include #include #include #include @@ -11,11 +10,6 @@ static std::ofstream ofs; -__attribute__((weak)) -extern "C" void AIBinder_Class_setTransactionCodeToFunctionNameMap(void*, const char* const*, size_t) { - -} - __attribute__((weak)) void __libcpp_verbose_abort(char const* format, ...) __asm("_ZNSt3__122__libcpp_verbose_abortEPKcz"); diff --git a/app/src/main/cpp/compat/a15.cpp b/app/src/main/cpp/compat/a15.cpp index b7c6b1aac..eaf54a51b 100644 --- a/app/src/main/cpp/compat/a15.cpp +++ b/app/src/main/cpp/compat/a15.cpp @@ -1 +1,7 @@ #include "a16.cpp" +#include + +__attribute__((weak)) +extern "C" void AIBinder_Class_setTransactionCodeToFunctionNameMap(void*, const char* const*, size_t) { + +} From 6e6bf51e3dde6c1ffe80eabc89b8d674d3237365 Mon Sep 17 00:00:00 2001 From: XiaoYeZi Date: Wed, 12 Aug 2026 01:28:12 +0800 Subject: [PATCH 122/182] qemu: enable multithreaded TCG translation --- .../classfun/droidvm/daemon/vm/backend/QemuBackendInstance.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/QemuBackendInstance.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/QemuBackendInstance.java index 8cff0bcd5..47a4fc447 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/QemuBackendInstance.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/QemuBackendInstance.java @@ -147,7 +147,7 @@ private List buildCommand() { defProtectedMode = ProtectedVM.PROTECTED_WITHOUT_FIRMWARE; break; case SOFT: - args.add("tcg"); + args.add("tcg,thread=multi,tb-size=128"); break; default:throw new IllegalArgumentException(fmt("Unsupported hypervisor: %s", hypervisor)); } From 699b579287d7005b60f0967f7a07e0ba5ac3905c Mon Sep 17 00:00:00 2001 From: XiaoYeZi Date: Wed, 12 Aug 2026 01:29:15 +0800 Subject: [PATCH 123/182] qemu: preserve configured vCPU count with SMT --- .../droidvm/daemon/vm/backend/QemuBackendInstance.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/QemuBackendInstance.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/QemuBackendInstance.java index 47a4fc447..5580c7461 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/QemuBackendInstance.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/QemuBackendInstance.java @@ -171,10 +171,10 @@ private List buildCommand() { args.add(cpu); long cpuCount = Math.max(item.optLong("cpu_count", 1), 1); boolean smt = item.optBoolean("smt", true); - long threads = smt ? 2 : 1; - long cores = Math.max(cpuCount / threads, 1); + long threads = smt && cpuCount % 2 == 0 ? 2 : 1; + long cores = cpuCount / threads; args.add("-smp"); - args.add(fmt("%d,sockets=1,cores=%d,threads=%d", cores * threads, cores, threads)); + args.add(fmt("%d,sockets=1,cores=%d,threads=%d", cpuCount, cores, threads)); long memMb = Math.max(item.optLong("memory_mb", 512), 64); args.add("-m"); args.add(fmt("%dM", memMb)); From 43fcc7a79edb31c0ef6f2457e7d788590ce58f24 Mon Sep 17 00:00:00 2001 From: lateautumn233 Date: Wed, 15 Jul 2026 12:24:10 +0800 Subject: [PATCH 124/182] feat: add multi-volume split for vm package export/import - split compressed stream into fixed-size .001/.002 sub-volumes with a metadata master holding header + manifest + volume index - verify sub-volume size and CRC32 on import and rebuild the logical byte stream - add volume_size input to export UI, default off (single file) - clean up partial and stale sub-volumes on export failure/re-run - reset volume status fields between operations to avoid stale UI --- .../droidvm/daemon/ipc/vm/ExportHandler.java | 7 + .../droidvm/daemon/vm/pkg/VMExportTask.java | 87 +++++++ .../droidvm/daemon/vm/pkg/VMExportUtils.java | 11 + .../droidvm/daemon/vm/pkg/VMImportTask.java | 63 +++-- .../droidvm/lib/archive/ZeroInputStream.java | 31 +++ .../droidvm/lib/pkg/PackageConstants.java | 11 + .../droidvm/lib/pkg/PackageHeader.java | 4 + .../droidvm/lib/pkg/PackageInput.java | 34 +++ .../classfun/droidvm/lib/pkg/VolumeIndex.java | 94 ++++++++ .../classfun/droidvm/lib/pkg/VolumeSet.java | 220 ++++++++++++++++++ .../lib/pkg/VolumeSplitOutputStream.java | 134 +++++++++++ .../droidvm/lib/utils/BinaryUtils.java | 10 + .../vm/pkg/exports/VMPkgExportActivity.java | 33 ++- .../vm/pkg/imports/VMPkgImportActivity.java | 46 +++- .../main/res/layout/activity_vmpkg_export.xml | 21 ++ app/src/main/res/values-zh-rCN/strings.xml | 5 + app/src/main/res/values-zh-rTW/strings.xml | 5 + app/src/main/res/values/strings.xml | 5 + 18 files changed, 797 insertions(+), 24 deletions(-) create mode 100644 app/src/main/java/cn/classfun/droidvm/lib/archive/ZeroInputStream.java create mode 100644 app/src/main/java/cn/classfun/droidvm/lib/pkg/VolumeIndex.java create mode 100644 app/src/main/java/cn/classfun/droidvm/lib/pkg/VolumeSet.java create mode 100644 app/src/main/java/cn/classfun/droidvm/lib/pkg/VolumeSplitOutputStream.java diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ExportHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ExportHandler.java index 28cf88ea9..dab0c1323 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ExportHandler.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ExportHandler.java @@ -8,6 +8,7 @@ import cn.classfun.droidvm.daemon.server.RequestException; import cn.classfun.droidvm.daemon.server.RequestHandler; import cn.classfun.droidvm.daemon.vm.pkg.VMExportTask; +import cn.classfun.droidvm.lib.pkg.PackageConstants; @AutoService(RequestHandler.class) public final class ExportHandler extends RequestHandler { @@ -25,6 +26,12 @@ public void handle(@NonNull ClientRequest request) throws Exception { throw new RequestException("missing vm_id"); if (destPath.isEmpty()) throw new RequestException("missing dest_path"); + var volumeSize = params.optLong("volume_size", 0); + if (volumeSize > 0 && volumeSize < PackageConstants.MIN_VOLUME_SIZE) + throw new RequestException(String.format( + "volume_size too small: %d (min %d)", + volumeSize, PackageConstants.MIN_VOLUME_SIZE + )); var store = request.getContext().getExportTaskStore(); var server = request.getClient().getServer(); var task = new VMExportTask(server, params); diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/pkg/VMExportTask.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/pkg/VMExportTask.java index b40674956..8a8520c39 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/pkg/VMExportTask.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/pkg/VMExportTask.java @@ -36,6 +36,8 @@ import cn.classfun.droidvm.lib.pkg.PackageConstants; import cn.classfun.droidvm.lib.pkg.PackageManifest; import cn.classfun.droidvm.lib.pkg.Phase; +import cn.classfun.droidvm.lib.pkg.VolumeIndex; +import cn.classfun.droidvm.lib.pkg.VolumeSplitOutputStream; import cn.classfun.droidvm.lib.store.base.DataItem; public final class VMExportTask { @@ -47,6 +49,9 @@ public final class VMExportTask { private final UUID sourceVMId; private final int totalItems; private final String destPath; + private final long volumeSize; + private VolumeSplitOutputStream vout = null; + private int finalVolumeCount = 0; public VMExportTask( @NonNull Server server, @@ -56,6 +61,7 @@ public VMExportTask( destPath = request.optString("dest_path"); if (!destPath.startsWith("/")) throw new IllegalArgumentException("missing dest_path"); + volumeSize = request.optLong("volume_size", 0); var vmId = UUID.fromString(request.optString("vm_id")); sourceVMId = vmId; var vm = server.getContext().getVMs().findById(vmId); @@ -91,6 +97,10 @@ public void startAsync() { try { startWrite(); data.set("done", totalItems); + if (finalVolumeCount > 0) { + data.set("volume_index", finalVolumeCount); + data.set("volume_total", finalVolumeCount); + } emit(data, Phase.DONE); } catch (Exception e) { Log.w(TAG, fmt("Export task %s failed", taskId), e); @@ -105,6 +115,11 @@ public void startAsync() { } private void startWrite() throws Exception { + if (volumeSize > 0) startWriteSplit(); + else startWriteSingle(); + } + + private void startWriteSingle() throws Exception { var outFile = new File(destPath); var manifestBytes = manifest.toJson().toString(4).getBytes(UTF_8); if (manifestBytes.length > 0xffff) @@ -132,6 +147,75 @@ private void startWrite() throws Exception { } } + // Multi-volume path: (1) slice the compressed data stream into .001, + // .002, ... sub-volumes, then (2) write the metadata master holding + // header + manifest + volume index (no data). The picked SAF file is reused + // as the master, so it is not deleted. + private void startWriteSplit() throws Exception { + var manifestBytes = manifest.toJson().toString(4).getBytes(UTF_8); + if (manifestBytes.length > 0xffff) + throw new IOException(fmt("manifest too large: %d bytes", manifestBytes.length)); + var subOut = new VolumeSplitOutputStream(destPath, volumeSize); + vout = subOut; + try { + try ( + var compressed = wrapCompressionOutput(subOut, manifest.compression); + var tar = new TarWriter(compressed) + ) { + tar.entry(PackageConstants.MANIFEST_NAME, manifestBytes); + pack(tar); + } + subOut.finish(); + } catch (Exception e) { + subOut.abort(); + deleteQuietly(subOut.files()); + throw e; + } finally { + vout = null; + } + long dataSize = subOut.dataSize(); + finalVolumeCount = subOut.volumeCount(); + writeMaster(manifestBytes, dataSize, subOut); + // Remove any higher-numbered sub-volumes left by a previous, larger + // export to the same base path, so the on-disk set matches the index. + for (int i = finalVolumeCount + 1; i <= PackageConstants.VOLUME_MAX_COUNT; i++) { + var stale = new File(VolumeSplitOutputStream.volumeName(destPath, i)); + if (!stale.exists()) break; + //noinspection ResultOfMethodCallIgnored + stale.delete(); + } + } + + private static void deleteQuietly(@NonNull java.util.List files) { + for (var f : files) { + //noinspection ResultOfMethodCallIgnored + f.delete(); + } + } + + private void writeMaster( + @NonNull byte[] manifestBytes, + long dataSize, + @NonNull VolumeSplitOutputStream subOut + ) throws Exception { + var index = new VolumeIndex(); + index.dataSize = dataSize; + index.volumes.addAll(subOut.entries()); + var indexBytes = index.toJson().toString().getBytes(UTF_8); + try (var raf = new RandomAccessFile(new File(destPath), "rw")) { + raf.setLength(0); + raf.write(new byte[PackageConstants.HEADER_SIZE]); + writeZero(raf, alignUp(raf.getFilePointer()) - raf.getFilePointer()); + raf.write(manifestBytes); + writeZero(raf, alignUpStrict(raf.getFilePointer()) - raf.getFilePointer()); + raf.write(indexBytes); + raf.seek(0); + raf.write(buildHeader( + manifest, manifestBytes.length, dataSize, finalVolumeCount + )); + } + } + private void pack(@NonNull TarWriter tar) throws Exception { int done; var data = DataItem.newObject(); @@ -209,6 +293,9 @@ private void emit(@NonNull DataItem item, @NonNull Phase phase) { data.put("phase", phase.name().toLowerCase()); data.put("vm_id", sourceVMId.toString()); data.put("vm_name", manifest.vm.getName()); + var out = vout; + if (out != null && !data.has("volume_index")) + data.put("volume_index", out.currentIndex()); var ev = new JSONObject(); ev.put("type", "event"); ev.put("data", data); diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/pkg/VMExportUtils.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/pkg/VMExportUtils.java index 2ed8a7934..83f382244 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/pkg/VMExportUtils.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/pkg/VMExportUtils.java @@ -58,6 +58,16 @@ public static byte[] buildHeader( @NonNull PackageManifest manifest, int manifestSize, long dataSize + ) throws IOException { + return buildHeader(manifest, manifestSize, dataSize, 0); + } + + @NonNull + public static byte[] buildHeader( + @NonNull PackageManifest manifest, + int manifestSize, + long dataSize, + int volumeCount ) throws IOException { var hdr = new byte[PackageConstants.HEADER_SIZE]; var magic = PackageConstants.MAGIC.getBytes(UTF_8); @@ -66,6 +76,7 @@ public static byte[] buildHeader( putUInt16LE(hdr, 8, BuildConfig.VERSION_CODE); putUInt16LE(hdr, 10, manifestSize); putUInt16LE(hdr, 12, manifest.compression.type); + putUInt16LE(hdr, 14, volumeCount); putInt64LE(hdr, 16, dataSize); return hdr; } diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/pkg/VMImportTask.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/pkg/VMImportTask.java index dcf56b774..9f7926007 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/pkg/VMImportTask.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/pkg/VMImportTask.java @@ -4,6 +4,7 @@ import static cn.classfun.droidvm.daemon.vm.pkg.VMImportUtils.remapDiskPaths; import static cn.classfun.droidvm.daemon.vm.pkg.VMImportUtils.uniqueFile; import static cn.classfun.droidvm.lib.pkg.PackageConstants.BUFFER; +import static cn.classfun.droidvm.lib.utils.BinaryUtils.readFully; import static cn.classfun.droidvm.lib.utils.JsonUtils.listToJSONArray; import static cn.classfun.droidvm.lib.utils.StringUtils.basename; import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; @@ -31,9 +32,12 @@ import cn.classfun.droidvm.lib.archive.TarReader; import cn.classfun.droidvm.lib.pkg.BootFile; import cn.classfun.droidvm.lib.pkg.DiskEntry; +import cn.classfun.droidvm.lib.pkg.PackageConstants; +import cn.classfun.droidvm.lib.pkg.PackageHeader; import cn.classfun.droidvm.lib.pkg.PackageInput; import cn.classfun.droidvm.lib.pkg.PackageManifest; import cn.classfun.droidvm.lib.pkg.Phase; +import cn.classfun.droidvm.lib.pkg.VolumeSet; import cn.classfun.droidvm.lib.store.base.DataItem; import cn.classfun.droidvm.lib.store.network.NetworkConfig; import cn.classfun.droidvm.lib.store.vm.VMConfig; @@ -47,6 +51,7 @@ public final class VMImportTask { private final File targetDir; private final String networkMode; private int totalItems; + private int volumeTotal = 0; public VMConfig importedVM = null; public PackageManifest importedManifest = null; public final ArrayList placedDisks = new ArrayList<>(); @@ -125,22 +130,24 @@ private void onTarItem( private void unpack() throws Exception { if (!targetDir.exists() && !targetDir.mkdirs()) throw new IOException(fmt("Cannot create target dir: %s", targetDir)); - try ( - var in = new FileInputStream(srcPath); - var pkg = PackageInput.open(in) - ) { - importedManifest = pkg.manifest; - totalItems = importedManifest.disks.size() + importedManifest.boots.size(); - var data = DataItem.newObject(); - data.set("done", 0); - data.set("total", totalItems); - emit(data, Phase.PACK); - var tar = new TarReader(pkg.data); - tar.forEach(this::onTarItem); - var buf = new byte[BUFFER]; - //noinspection StatementWithEmptyBody - while (pkg.data.read(buf) >= 0); - pkg.validateDataConsumed(); + // Any picked path maps to its metadata master (strip a .NNN suffix). + var masterPath = VolumeSet.masterOf(srcPath); + if (readVolumeCount(masterPath) > 0) { + var set = VolumeSet.discover(masterPath); + volumeTotal = set.count(); + try ( + var in = set.openLogicalStream(); + var pkg = PackageInput.open(in, set.dataSize()) + ) { + extract(pkg); + } + } else { + try ( + var in = new FileInputStream(masterPath); + var pkg = PackageInput.open(in) + ) { + extract(pkg); + } } var vm = new VMConfig(importedManifest.vm.toJson()); vm.setId(UUID.randomUUID()); @@ -149,6 +156,29 @@ private void unpack() throws Exception { importedVM = vm; } + private int readVolumeCount(@NonNull String masterPath) throws Exception { + try (var in = new FileInputStream(masterPath)) { + var hdr = new byte[PackageConstants.HEADER_SIZE]; + readFully(in, hdr); + return PackageHeader.fromBytes(hdr).volumeCount; + } + } + + private void extract(@NonNull PackageInput pkg) throws Exception { + importedManifest = pkg.manifest; + totalItems = importedManifest.disks.size() + importedManifest.boots.size(); + var data = DataItem.newObject(); + data.set("done", 0); + data.set("total", totalItems); + emit(data, Phase.PACK); + var tar = new TarReader(pkg.data); + tar.forEach(this::onTarItem); + var buf = new byte[BUFFER]; + //noinspection StatementWithEmptyBody + while (pkg.data.read(buf) >= 0); + pkg.validateDataConsumed(); + } + @NonNull private JSONArray importNetworks( @NonNull VMConfig vm, @@ -281,6 +311,7 @@ private void emit(@NonNull DataItem item, @NonNull Phase phase) { data.put("event", "vm_import_status"); data.put("task_id", taskId.toString()); data.put("phase", phase.name().toLowerCase()); + if (volumeTotal > 0) data.put("volume_total", volumeTotal); var ev = new JSONObject(); ev.put("type", "event"); ev.put("data", data); diff --git a/app/src/main/java/cn/classfun/droidvm/lib/archive/ZeroInputStream.java b/app/src/main/java/cn/classfun/droidvm/lib/archive/ZeroInputStream.java new file mode 100644 index 000000000..00e899a6b --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/lib/archive/ZeroInputStream.java @@ -0,0 +1,31 @@ +package cn.classfun.droidvm.lib.archive; + +import androidx.annotation.NonNull; + +import java.io.InputStream; +import java.util.Arrays; + +/** A bounded stream of zero bytes, used to reproduce trailing alignment padding. */ +public final class ZeroInputStream extends InputStream { + private long remaining; + + public ZeroInputStream(long count) { + this.remaining = Math.max(0, count); + } + + @Override + public int read() { + if (remaining <= 0) return -1; + remaining--; + return 0; + } + + @Override + public int read(@NonNull byte[] b, int off, int len) { + if (remaining <= 0) return -1; + int n = (int) Math.min(len, remaining); + Arrays.fill(b, off, off + n, (byte) 0); + remaining -= n; + return n; + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/lib/pkg/PackageConstants.java b/app/src/main/java/cn/classfun/droidvm/lib/pkg/PackageConstants.java index bca0dadfd..c45a8af77 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/pkg/PackageConstants.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/pkg/PackageConstants.java @@ -12,6 +12,17 @@ public final class PackageConstants { public static final String MANIFEST_NAME = "manifest.json"; public static final Compression DEFAULT_COMPRESSION = Compression.ZSTD; + // Multi-volume split. The picked file stays as a small metadata + // master (header + manifest + volume index); the compressed data stream is + // sliced across .001, .002, ... sub-volumes. volume_size <= 0 + // keeps the legacy single self-contained file (volumeCount == 0). + public static final long DEFAULT_VOLUME_SIZE = 500L * 1024 * 1024; + public static final long MIN_VOLUME_SIZE = 16L * 1024 * 1024; + public static final String VOLUME_MAGIC = "VMPV"; + public static final int VOLUME_VERSION = 1; + public static final int VOLUME_MAX_COUNT = 999; + public static final int VOLUME_SUFFIX_DIGITS = 3; + private PackageConstants() { } } diff --git a/app/src/main/java/cn/classfun/droidvm/lib/pkg/PackageHeader.java b/app/src/main/java/cn/classfun/droidvm/lib/pkg/PackageHeader.java index 08d934b12..64956cf52 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/pkg/PackageHeader.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/pkg/PackageHeader.java @@ -18,6 +18,9 @@ public final class PackageHeader { public int appVersionCode = 0; public int manifestSize = 0; public int compression = 0; + // 0 = single self-contained file; > 0 = split package whose data lives in + // that many .NNN sub-volumes (this file is the metadata master). + public int volumeCount = 0; public long dataSize = 0; public PackageHeader() { @@ -32,6 +35,7 @@ public void parseFromData(@NonNull byte[] hdr) throws IOException { appVersionCode = getUInt16LE(hdr, 8); manifestSize = getUInt16LE(hdr, 10); compression = getUInt16LE(hdr, 12); + volumeCount = getUInt16LE(hdr, 14); dataSize = getInt64LE(hdr, 16); } diff --git a/app/src/main/java/cn/classfun/droidvm/lib/pkg/PackageInput.java b/app/src/main/java/cn/classfun/droidvm/lib/pkg/PackageInput.java index 7a35024ad..397c2b1e5 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/pkg/PackageInput.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/pkg/PackageInput.java @@ -55,6 +55,39 @@ public void close() throws IOException { @NonNull public static PackageInput open(@NonNull InputStream raw) throws Exception { + return open(raw, -1); + } + + /** + * Reads just the header and manifest for preview, without setting up the + * data/decompression stream. Safe on a metadata master (whose bytes after + * the manifest are the volume index, not compressed data). + */ + @NonNull + public static PackageManifest peekManifest(@NonNull InputStream raw) throws Exception { + var pkgHeader = PackageHeader.fromStream(raw); + var manifest = PackageManifest.fromStream(raw, pkgHeader); + if (pkgHeader.manifestVersion != manifest.manifestVersion) + throw new IOException("vmpkg manifest_version mismatch"); + if (pkgHeader.appVersionCode != manifest.appVersionCode) + throw new IOException("vmpkg app_version_code mismatch"); + if (pkgHeader.compression != manifest.compression.type) + throw new IOException("vmpkg compression mismatch"); + return manifest; + } + + /** + * Opens a package, optionally overriding the data-region size. Multi-volume + * packages write a placeholder {@code dataSize} of 0 in the inline header + * (it is only known after streaming) and carry the real value in the volume + * trailer; pass it here as {@code dataSizeOverride}. Use -1 for the legacy + * single-file layout that patches its header in place. + */ + @NonNull + public static PackageInput open( + @NonNull InputStream raw, + long dataSizeOverride + ) throws Exception { var pkgHeader = PackageHeader.fromStream(raw); var manifest = PackageManifest.fromStream(raw, pkgHeader); if (pkgHeader.manifestVersion != manifest.manifestVersion) @@ -63,6 +96,7 @@ public static PackageInput open(@NonNull InputStream raw) throws Exception { throw new IOException("vmpkg app_version_code mismatch"); if (pkgHeader.compression != manifest.compression.type) throw new IOException("vmpkg compression mismatch"); + if (dataSizeOverride >= 0) pkgHeader.dataSize = dataSizeOverride; long pos = alignUp(PackageConstants.HEADER_SIZE) + pkgHeader.manifestSize; readZeroPadding(raw, alignUpStrict(pos) - pos); var stream = new LimitedInputStream(raw, pkgHeader.dataSize); diff --git a/app/src/main/java/cn/classfun/droidvm/lib/pkg/VolumeIndex.java b/app/src/main/java/cn/classfun/droidvm/lib/pkg/VolumeIndex.java new file mode 100644 index 000000000..bb6ad12eb --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/lib/pkg/VolumeIndex.java @@ -0,0 +1,94 @@ +package cn.classfun.droidvm.lib.pkg; + +import static cn.classfun.droidvm.lib.utils.JsonUtils.arrayToList; +import static cn.classfun.droidvm.lib.utils.JsonUtils.listToJSONArray; +import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; + +import androidx.annotation.NonNull; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import cn.classfun.droidvm.lib.store.base.JSONSerialize; + +/** + * Volume list embedded in the metadata master ({@code .vmpkg}) right + * after the manifest. It records the total data size and, for each sub-volume, + * its logical size and CRC32 so a reader can verify the set is complete and + * intact before unpacking. Plain JSON -- its length is master-file-size minus + * the data-region offset, so no framing/magic footer is needed. + */ +public final class VolumeIndex implements JSONSerialize { + public static final class Entry implements JSONSerialize { + public final int index; + public final long size; + public final long crc32; + + public Entry(int index, long size, long crc32) { + this.index = index; + this.size = size; + this.crc32 = crc32; + } + + public Entry(@NonNull JSONObject o) { + index = o.optInt("index"); + size = o.optLong("size"); + crc32 = o.optLong("crc32"); + } + + @NonNull + @Override + public JSONObject toJson() throws JSONException { + var o = new JSONObject(); + o.put("index", index); + o.put("size", size); + o.put("crc32", crc32); + return o; + } + + @NonNull + public static Entry from(Object o) throws JSONException { + if (o instanceof JSONObject) + return new Entry((JSONObject) o); + throw new JSONException("object is not json"); + } + } + + public int version = PackageConstants.VOLUME_VERSION; + public long dataSize = 0; + public final List volumes = new ArrayList<>(); + + public VolumeIndex() { + } + + public VolumeIndex(@NonNull JSONObject o) throws IOException, JSONException { + if (!PackageConstants.VOLUME_MAGIC.equals(o.optString("magic"))) + throw new IOException("missing volume index magic"); + version = o.optInt("version"); + if (version != PackageConstants.VOLUME_VERSION) + throw new IOException(fmt("unsupported volume version: %d", version)); + dataSize = o.optLong("data_size"); + volumes.addAll(arrayToList(o, "volumes", Entry::from)); + if (volumes.isEmpty()) + throw new IOException("volume index has no volumes"); + } + + public VolumeIndex(@NonNull String json) throws IOException, JSONException { + this(new JSONObject(json)); + } + + @NonNull + @Override + public JSONObject toJson() throws JSONException { + var o = new JSONObject(); + o.put("magic", PackageConstants.VOLUME_MAGIC); + o.put("version", version); + o.put("data_size", dataSize); + o.put("volumes", listToJSONArray(volumes)); + return o; + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/lib/pkg/VolumeSet.java b/app/src/main/java/cn/classfun/droidvm/lib/pkg/VolumeSet.java new file mode 100644 index 000000000..afd1b1ce8 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/lib/pkg/VolumeSet.java @@ -0,0 +1,220 @@ +package cn.classfun.droidvm.lib.pkg; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static cn.classfun.droidvm.lib.utils.BinaryUtils.alignUp; +import static cn.classfun.droidvm.lib.utils.BinaryUtils.alignUpStrict; +import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; + +import androidx.annotation.NonNull; + +import org.json.JSONException; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.SequenceInputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Vector; +import java.util.zip.CRC32; + +import cn.classfun.droidvm.lib.archive.ZeroInputStream; + +/** + * Reader-side view of a multi-volume vmpkg. Given the metadata master + * ({@code .vmpkg}), it parses the embedded {@link VolumeIndex}, locates + * and validates the {@code .001 ..} sub-volumes, then rebuilds the exact + * single-file byte stream -- {@code [master head][sub-volume data][tail padding]} + * -- so the unchanged {@link PackageInput} can consume it. Sub-volume CRC32s are + * verified as their boundaries are crossed. + */ +public final class VolumeSet { + private final byte[] masterHead; + private final long dataStart; + private final VolumeIndex index; + private final List subVolumes; + + private VolumeSet( + @NonNull byte[] masterHead, + long dataStart, + @NonNull VolumeIndex index, + @NonNull List subVolumes + ) { + this.masterHead = masterHead; + this.dataStart = dataStart; + this.index = index; + this.subVolumes = subVolumes; + } + + /** True when {@code path} looks like a sub-volume member ({@code ....NNN}). */ + public static boolean isSubVolume(@NonNull String path) { + return path.matches(fmt(".*\\.\\d{%d}", PackageConstants.VOLUME_SUFFIX_DIGITS)); + } + + /** Maps any picked path to its metadata master: strips a {@code .NNN} + * sub-volume suffix, or returns the path unchanged for the master itself. */ + @NonNull + public static String masterOf(@NonNull String path) { + if (!isSubVolume(path)) return path; + return path.substring(0, path.length() - PackageConstants.VOLUME_SUFFIX_DIGITS - 1); + } + + public long dataSize() { + return index.dataSize; + } + + public int count() { + return subVolumes.size(); + } + + /** + * Parses and validates the whole set from the master path. Throws with a + * clear message when a sub-volume is missing or a size does not match. + */ + @NonNull + public static VolumeSet discover(@NonNull String masterPath) throws IOException { + var master = new File(masterPath); + var all = readAllBytes(master); + if (all.length < PackageConstants.HEADER_SIZE) + throw new IOException("master too small"); + PackageHeader hdr; + try { + hdr = PackageHeader.fromBytes(Arrays.copyOf(all, PackageConstants.HEADER_SIZE)); + } catch (IOException e) { + throw e; + } catch (Exception e) { + throw new IOException("invalid master header", e); + } + if (hdr.volumeCount <= 0) + throw new IOException("not a multi-volume package"); + long dataStart = alignUpStrict(alignUp(PackageConstants.HEADER_SIZE) + hdr.manifestSize); + if (all.length < dataStart) + throw new IOException("truncated master"); + var indexJson = new String(all, (int) dataStart, all.length - (int) dataStart, UTF_8); + VolumeIndex index; + try { + index = new VolumeIndex(indexJson); + } catch (JSONException e) { + throw new IOException("failed to parse volume index", e); + } + if (index.volumes.size() != hdr.volumeCount) + throw new IOException(fmt( + "volume count mismatch: header %d, index %d", + hdr.volumeCount, index.volumes.size() + )); + var subs = new ArrayList(); + for (int i = 0; i < index.volumes.size(); i++) { + var entry = index.volumes.get(i); + var f = new File(VolumeSplitOutputStream.volumeName(masterPath, i + 1)); + if (!f.isFile()) + throw new IOException(fmt("missing sub-volume %d (%s)", i + 1, f.getName())); + if (f.length() != entry.size) + throw new IOException(fmt( + "sub-volume %d size mismatch: on-disk %d, expected %d", + i + 1, f.length(), entry.size + )); + subs.add(f); + } + var head = Arrays.copyOf(all, (int) dataStart); + return new VolumeSet(head, dataStart, index, subs); + } + + /** + * Rebuilds the single-file byte stream: master head (header + manifest + + * padding) ++ concatenated sub-volume data (CRC-checked) ++ trailing + * alignment zeros. Feed to {@link PackageInput#open(InputStream, long)}. + */ + @NonNull + public InputStream openLogicalStream() { + long dataEnd = dataStart + index.dataSize; + long tailPad = alignUp(dataEnd) - dataEnd; + var streams = new Vector(); + streams.add(new ByteArrayInputStream(masterHead)); + streams.add(new SubVolumeConcatStream()); + streams.add(new ZeroInputStream(tailPad)); + return new SequenceInputStream(streams.elements()); + } + + @NonNull + private static byte[] readAllBytes(@NonNull File f) throws IOException { + long len = f.length(); + if (len <= 0 || len > (16L << 20)) + throw new IOException(fmt("unexpected master size: %d", len)); + var buf = new byte[(int) len]; + try (var in = new FileInputStream(f)) { + int off = 0; + while (off < buf.length) { + int n = in.read(buf, off, buf.length - off); + if (n < 0) throw new IOException("short read on master"); + off += n; + } + } + return buf; + } + + private final class SubVolumeConcatStream extends InputStream { + private int idx = -1; + private InputStream cur; + private CRC32 crc; + private long remaining; + + private boolean advance() throws IOException { + if (cur != null) { + verify(); + cur.close(); + cur = null; + } + idx++; + if (idx >= subVolumes.size()) return false; + cur = new BufferedInputStream( + new FileInputStream(subVolumes.get(idx)), PackageConstants.BUFFER + ); + crc = new CRC32(); + remaining = index.volumes.get(idx).size; + return true; + } + + private void verify() throws IOException { + if (remaining != 0) + throw new IOException(fmt("sub-volume %d truncated", idx + 1)); + if (crc.getValue() != index.volumes.get(idx).crc32) + throw new IOException(fmt("sub-volume %d checksum mismatch", idx + 1)); + } + + @Override + public int read() throws IOException { + var one = new byte[1]; + int n = read(one, 0, 1); + return n < 0 ? -1 : (one[0] & 0xff); + } + + @Override + public int read(@NonNull byte[] b, int off, int len) throws IOException { + while (true) { + if (cur == null || remaining == 0) { + if (!advance()) return -1; + continue; + } + int toRead = (int) Math.min(len, remaining); + int n = cur.read(b, off, toRead); + if (n < 0) + throw new IOException(fmt("sub-volume %d ended early", idx + 1)); + crc.update(b, off, n); + remaining -= n; + return n; + } + } + + @Override + public void close() throws IOException { + if (cur != null) { + cur.close(); + cur = null; + } + } + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/lib/pkg/VolumeSplitOutputStream.java b/app/src/main/java/cn/classfun/droidvm/lib/pkg/VolumeSplitOutputStream.java new file mode 100644 index 000000000..77a216f83 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/lib/pkg/VolumeSplitOutputStream.java @@ -0,0 +1,134 @@ +package cn.classfun.droidvm.lib.pkg; + +import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; + +import androidx.annotation.NonNull; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.zip.CRC32; + +/** + * Slices the compressed data stream across fixed-size sub-volumes + * {@code .001, .002, ...}. Pure data: no header, manifest or magic + * -- those live in the metadata master ({@link VolumeIndex}). Tracks each + * sub-volume's size and CRC32. {@link #close()} is a no-op so an upstream + * compression stream can be closed without ending the volume set; finalize + * explicitly with {@link #finish()} and read {@link #entries()}/{@link #dataSize()}. + */ +public final class VolumeSplitOutputStream extends OutputStream { + private final String basePath; + private final long volumeSize; + private final List entries = new ArrayList<>(); + private final List files = new ArrayList<>(); + private OutputStream current; + private CRC32 currentCrc; + private long currentBytes; + private int index; + private long position; + private boolean finished; + + public VolumeSplitOutputStream(@NonNull String basePath, long volumeSize) { + this.basePath = basePath; + this.volumeSize = volumeSize; + } + + @NonNull + public static String volumeName(@NonNull String basePath, int index) { + return fmt("%s.%0" + PackageConstants.VOLUME_SUFFIX_DIGITS + "d", basePath, index); // concat-ok + } + + @Override + public void write(int b) throws IOException { + write(new byte[]{(byte) b}, 0, 1); + } + + @Override + public void write(@NonNull byte[] b, int off, int len) throws IOException { + if (finished) throw new IOException("volume stream already finished"); + while (len > 0) { + if (current == null || currentBytes >= volumeSize) rollVolume(); + int chunk = (int) Math.min(len, volumeSize - currentBytes); + current.write(b, off, chunk); + currentCrc.update(b, off, chunk); + currentBytes += chunk; + position += chunk; + off += chunk; + len -= chunk; + } + } + + private void rollVolume() throws IOException { + if (current != null) closeCurrent(); + index++; + if (index > PackageConstants.VOLUME_MAX_COUNT) + throw new IOException(fmt( + "too many volumes (> %d); use a larger volume size", + PackageConstants.VOLUME_MAX_COUNT + )); + var file = new File(volumeName(basePath, index)); + files.add(file); + current = new BufferedOutputStream(new FileOutputStream(file), PackageConstants.BUFFER); + currentCrc = new CRC32(); + currentBytes = 0; + } + + private void closeCurrent() throws IOException { + entries.add(new VolumeIndex.Entry(index, currentBytes, currentCrc.getValue())); + current.flush(); + current.close(); + current = null; + } + + /** Closes the final sub-volume and records its entry. */ + public void finish() throws IOException { + if (finished) return; + if (current != null) closeCurrent(); + finished = true; + } + + /** Total bytes written across all sub-volumes (the compressed data size). */ + public long dataSize() { + return position; + } + + public int currentIndex() { + return index; + } + + public int volumeCount() { + return entries.size(); + } + + @NonNull + public List entries() { + return entries; + } + + @NonNull + public List files() { + return files; + } + + /** No-op: mirrors RandomAccessFileOutputStream so upstream close() cannot + * end the volume set prematurely. Use {@link #finish()} to finalize. */ + @Override + public void close() { + } + + /** Closes any open sub-volume without recording it, for error cleanup. */ + public void abort() { + if (current != null) { + try { + current.close(); + } catch (IOException ignored) { + } + current = null; + } + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/lib/utils/BinaryUtils.java b/app/src/main/java/cn/classfun/droidvm/lib/utils/BinaryUtils.java index 38a7c7d7f..fb1039c63 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/utils/BinaryUtils.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/utils/BinaryUtils.java @@ -8,6 +8,7 @@ import java.io.EOFException; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.nio.charset.CodingErrorAction; @@ -45,6 +46,15 @@ public static void writeZero(@NonNull RandomAccessFile raf, long count) throws I } } + public static void writeZero(@NonNull OutputStream out, long count) throws IOException { + long rem = count; + while (rem > 0) { + int n = (int) Math.min(ZERO.length, rem); + out.write(ZERO, 0, n); + rem -= n; + } + } + @NonNull public static String decodeUtf8(@NonNull byte[] bytes) throws IOException { var decoder = UTF_8.newDecoder() diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/exports/VMPkgExportActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/exports/VMPkgExportActivity.java index 0a88f7568..74650153b 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/exports/VMPkgExportActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/exports/VMPkgExportActivity.java @@ -54,6 +54,7 @@ import cn.classfun.droidvm.lib.utils.ShareUtils; import cn.classfun.droidvm.ui.widgets.container.CollapsibleContainer; import cn.classfun.droidvm.ui.widgets.row.ChooseRowWidget; +import cn.classfun.droidvm.ui.widgets.row.TextInputRowWidget; public final class VMPkgExportActivity extends AppCompatActivity implements DaemonConnection.EventListener { @@ -70,6 +71,7 @@ public final class VMPkgExportActivity extends AppCompatActivity private TextView tvDiskSummary; private ChooseRowWidget chooseCompression; private TextView tvCompressionDesc; + private TextInputRowWidget inputVolumeSize; private LinearProgressIndicator pbRun; private TextView tvStatus; private TextView tvFileName; @@ -78,9 +80,11 @@ public final class VMPkgExportActivity extends AppCompatActivity private ExtendedFloatingActionButton fabShare; private VMConfig config; private Compression compression = PackageConstants.DEFAULT_COMPRESSION; + private long volumeSize = 0; private VMPkgExportDiskAdapter adapter; private String pendingTaskId = null; private String path = null; + private int exportVolumeIndex = 0; private boolean exporting = false; private Phase phase = Phase.IDLE; private final ActivityResultLauncher createDocLauncher = @@ -108,6 +112,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { tvDiskSummary = findViewById(R.id.tv_disk_summary); chooseCompression = findViewById(R.id.choose_compression); tvCompressionDesc = findViewById(R.id.tv_compression_desc); + inputVolumeSize = findViewById(R.id.input_volume_size); pbRun = findViewById(R.id.pb_run); tvStatus = findViewById(R.id.tv_status); tvFileName = findViewById(R.id.tv_filename); @@ -144,6 +149,9 @@ private void initialize() { containerDisks.setAdapter(adapter); bindDisks(); bindCompression(); + // Volume split defaults off: leave the input empty so export produces a + // single self-contained file unless the user enters a size. + inputVolumeSize.setText(""); var handler = new VMPkgExportOnBackPressedCallback(); toolbar.setNavigationOnClickListener(v -> handler.handleOnBackPressed()); getOnBackPressedDispatcher().addCallback(this, handler); @@ -303,6 +311,23 @@ private boolean validateSelectedDisks() { private void onDocPicked(@Nullable Uri uri) { if (uri == null || exporting) return; + // Clear any stale volume index from a previous export so a following + // single-file export does not inherit its "volume N" status. + exportVolumeIndex = 0; + // Read the (empty = single file) volume size on the UI thread before + // handing off to the worker pool. + var vsText = inputVolumeSize.getText().trim(); + if (vsText.isEmpty()) { + volumeSize = 0; + } else if (!inputVolumeSize.isInputValid()) { + showToast(R.string.vmpkg_export_failed, getString( + R.string.vmpkg_export_volume_size_invalid, + PackageConstants.MIN_VOLUME_SIZE / (1024 * 1024) + )); + return; + } else { + volumeSize = inputVolumeSize.getValue(); + } runOnPool(() -> startExport(uri)); } @@ -344,6 +369,7 @@ private void startExport(@NonNull Uri uri) { .put("disks", diskIndices) .put("dest_path", destPath) .put("compression", compression) + .put("volume_size", volumeSize) .onResponse(onExport) .onUnsuccessful(f) .onError(err) @@ -384,6 +410,7 @@ public void onDaemonEvent(JSONObject msg) { var bytesTotal = data.optLong("bytes_total"); var file = data.optString("file"); var message = data.optString("message"); + exportVolumeIndex = data.optInt("volume_index", exportVolumeIndex); mainHandler.post(() -> onProgress( phase, done, total, bytesDone, bytesTotal, file, message )); @@ -424,7 +451,10 @@ private void onProgress( applyProgress(done, total, bytesDone, bytesTotal); break; case PACK: - tvStatus.setText(R.string.vmpkg_export_pack); + if (exportVolumeIndex > 0) tvStatus.setText(getString( + R.string.vmpkg_export_pack_volume, exportVolumeIndex + )); + else tvStatus.setText(R.string.vmpkg_export_pack); applyProgress(done, total, bytesDone, bytesTotal); applyProgressDetail(file, bytesDone, bytesTotal); break; @@ -471,6 +501,7 @@ private void setExporting(boolean exporting) { pbRun.setVisibility(exporting ? VISIBLE : GONE); pbRun.setIndeterminate(exporting); chooseCompression.setEnabled(!exporting); + inputVolumeSize.setEnabled(!exporting); containerDisks.setEnabled(!exporting); adapter.setEnabled(!exporting); } diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/imports/VMPkgImportActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/imports/VMPkgImportActivity.java index e3283c07b..9689b5501 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/imports/VMPkgImportActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/imports/VMPkgImportActivity.java @@ -41,6 +41,7 @@ import org.json.JSONObject; import java.io.File; +import java.io.FileInputStream; import java.io.IOException; import java.text.DateFormat; import java.util.Date; @@ -51,6 +52,7 @@ import cn.classfun.droidvm.lib.pkg.PackageInput; import cn.classfun.droidvm.lib.pkg.PackageManifest; import cn.classfun.droidvm.lib.pkg.Phase; +import cn.classfun.droidvm.lib.pkg.VolumeSet; import cn.classfun.droidvm.lib.store.disk.DiskConfig; import cn.classfun.droidvm.lib.store.disk.DiskStore; import cn.classfun.droidvm.lib.store.network.NetworkConfig; @@ -86,10 +88,14 @@ public final class VMPkgImportActivity extends AppCompatActivity private TextView tvProgressDetail; private View groupSummary; private Uri pickedUri; + // Real filesystem path of the metadata master, resolved at pick time when a + // sub-volume (.NNN) is chosen so preview/import target the master directly. + private String masterRealPath; private PackageManifest preview; private VMPkgImportDiskAdapter diskAdapter; private VMPkgImportNetworkAdapter networkAdapter; private String pendingTaskId = null; + private int importVolumeTotal = 0; private boolean importing = false; private NetworkImportMode networkMode = NetworkImportMode.AUTO; private final ActivityResultLauncher openDocLauncher = @@ -188,16 +194,15 @@ protected void onStop() { private void onDocPicked(@Nullable Uri uri) { if (uri == null || importing) return; pickedUri = uri; + masterRealPath = null; + importVolumeTotal = 0; tvError.setVisibility(GONE); groupSummary.setVisibility(GONE); btnImport.setEnabled(false); tvStatus.setText(R.string.vmpkg_import_reading); runOnPool(() -> { - try (var is = getContentResolver().openInputStream(uri)) { - if (is == null) throw new RuntimeException("Cannot open file"); - try (var input = PackageInput.open(is)) { - preview = input.manifest; - } + try { + preview = readPreviewManifest(uri); mainHandler.post(this::showPreview); } catch (Exception e) { mainHandler.post(() -> showError(getString(R.string.vmpkg_import_invalid, e.getMessage()))); @@ -205,6 +210,26 @@ private void onDocPicked(@Nullable Uri uri) { }); } + @NonNull + private PackageManifest readPreviewManifest(@NonNull Uri uri) throws Exception { + // A picked sub-volume (.NNN) has no header/manifest; redirect to its + // metadata master in the same folder (needs MANAGE_EXTERNAL_STORAGE). + var real = resolveUriPath(this, uri); + if (real != null && !real.isEmpty() && VolumeSet.isSubVolume(real)) { + masterRealPath = VolumeSet.masterOf(real); + try (var is = new FileInputStream(masterRealPath)) { + return PackageInput.peekManifest(is); + } + } + // Master or single file: SAF stream is enough. Remember the resolved + // master path (may be null) so import can reuse it. + masterRealPath = real == null || real.isEmpty() ? null : VolumeSet.masterOf(real); + try (var is = getContentResolver().openInputStream(uri)) { + if (is == null) throw new RuntimeException("Cannot open file"); + return PackageInput.peekManifest(is); + } + } + @SuppressLint("NotifyDataSetChanged") private void showPreview() { if (preview == null) return; @@ -252,7 +277,10 @@ private void showPreview() { private void doImport() { if (pickedUri == null || preview == null || importing) return; - var srcPath = resolveUriPath(this, pickedUri); + // Prefer the master path resolved at pick time; the daemon maps any + // .NNN sub-volume to its master anyway. + var srcPath = masterRealPath; + if (srcPath == null || srcPath.isEmpty()) srcPath = resolveUriPath(this, pickedUri); if (srcPath == null || srcPath.isEmpty()) { showError(getString( R.string.vmpkg_import_failed, @@ -296,6 +324,7 @@ public void onDaemonEvent(JSONObject msg) { var phase = optEnum(data, "phase", Phase.SCAN); var done = data.optInt("done", 0); var total = data.optInt("total", 0); + importVolumeTotal = data.optInt("volume_total", importVolumeTotal); var file = data.optString("file", ""); var bytesDone = data.optLong("bytes_done", -1); var bytesTotal = data.optLong("bytes_total", -1); @@ -332,7 +361,10 @@ private void onProgress( ) { switch (phase) { case PACK: - tvStatus.setText(R.string.vmpkg_import_running); + if (importVolumeTotal > 0) tvStatus.setText(getString( + R.string.vmpkg_import_running_volume, importVolumeTotal + )); + else tvStatus.setText(R.string.vmpkg_import_running); applyProgress(done, total, bytesDone, bytesTotal); applyProgressDetail(file, bytesDone, bytesTotal); break; diff --git a/app/src/main/res/layout/activity_vmpkg_export.xml b/app/src/main/res/layout/activity_vmpkg_export.xml index f7a850bdb..7d47d8e14 100644 --- a/app/src/main/res/layout/activity_vmpkg_export.xml +++ b/app/src/main/res/layout/activity_vmpkg_export.xml @@ -153,6 +153,27 @@ android:textAppearance="?attr/textAppearanceBodySmall" android:textColor="?android:attr/textColorSecondary" /> + + + + 正在扫描磁盘\u2026 正在打包磁盘\u2026 %1$s / %2$s %3$d%% + 分卷大小 + 将包拆分为固定大小的分卷(.001、.002…)。留空则导出为单个文件。 + 分卷大小不能小于 %1$d MB + 正在打包磁盘(分卷 %1$d)… + 正在导入(共 %1$d 卷)… 包已创建 包已创建:%1$s 导出失败:%1$s diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index d6d5a0956..785db36f6 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -1056,6 +1056,11 @@ 正在掃描磁碟\u2026 正在打包磁碟\u2026 %1$s / %2$s %3$d%% + 分卷大小 + 將封裝包拆分為固定大小的分卷(.001、.002…)。留空則匯出為單一檔案。 + 分卷大小不能小於 %1$d MB + 正在打包磁碟(分卷 %1$d)… + 正在匯入(共 %1$d 卷)… 包已建立 包已建立:%1$s 匯出失敗:%1$s diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 85f08a344..954a15f63 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1089,9 +1089,13 @@ Gzip is fast with low CPU usage but produces larger archives. XZ (LZMA2) is slowest and CPU-heavy but produces the smallest archives. Zstandard offers the best balance: fast compression with good ratios. + Volume size + Split the package into fixed-size volumes (.001, .002, \u2026). Leave empty for a single file. + Volume size must be at least %1$d MB Submitting to daemon\u2026 Scanning disks\u2026 Packaging disks\u2026 + Packaging disks (volume %1$d)\u2026 %1$s / %2$s %3$d%% Package created Package created: %1$s @@ -1110,6 +1114,7 @@ Import Reading package\u2026 Importing\u2026 + Importing (%1$d volumes)\u2026 Disks Networks No disks in this package. From 05d6a5aba23ef42f7c84588d0872d45dd45d6cc3 Mon Sep 17 00:00:00 2001 From: Kancy Joe <54024877+sunflower2333@users.noreply.github.com> Date: Sun, 16 Aug 2026 14:35:00 +0000 Subject: [PATCH 125/182] feat(uefi): move pflash to UEFI vars under boot section - Add UEFI vars (pflash) config under boot.uefi: vars path + vars_enabled switch; empty vars = built-in edk2-gunyah.vars.fd (shared crosvm/QEMU) - crosvm backend: attach --pflash with auto block size when vars enabled; QEMU backend: attach -drive if=pflash with the same vars file - Boot tab UI: enable switch + vars path row (empty = built-in), browse dialog for custom vars file; hint text moved above the switch - Storage tab: PFLASH bus hidden while UEFI vars pflash is enabled; only one PFLASH disk allowed (mutual exclusion), auto-downgrade to VIRTIO - Validate pflash image size (8 MiB crosvm limit) on save for both the boot vars file and storage PFLASH disks; QEMU backend exempt - Add VMBackend accessor on the basic tab for cross-tab backend checks --- .../classfun/droidvm/daemon/vm/BootPlan.java | 27 +++++-- .../vm/backend/CrosvmBackendInstance.java | 17 +++- .../vm/backend/QemuBackendInstance.java | 6 ++ .../cn/classfun/droidvm/lib/Constants.java | 3 + .../droidvm/lib/store/vm/BootConfig.java | 21 ++++- .../ui/vm/edit/basic/VMEditBasicTab.java | 7 ++ .../ui/vm/edit/boot/VMEditBootTab.java | 81 +++++++++++++++++++ .../ui/vm/edit/storage/VMEditStorageTab.java | 46 ++++++++++- .../edit/storage/disk/VMDiskEditAdapter.java | 63 ++++++++++++++- .../main/res/layout/partial_vm_edit_boot.xml | 19 +++++ app/src/main/res/values-zh-rCN/strings.xml | 5 ++ app/src/main/res/values-zh-rTW/strings.xml | 5 ++ app/src/main/res/values/strings.xml | 5 ++ 13 files changed, 287 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/BootPlan.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/BootPlan.java index 87f10bd7b..39a097b8b 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/BootPlan.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/BootPlan.java @@ -60,6 +60,11 @@ public final class BootPlan { /** Custom UEFI firmware path; empty = builtin (QEMU honors, crosvm ignores). */ @NonNull public final String firmware; + /** Custom UEFI vars path; empty = builtin EDK2 vars. */ + @NonNull + public final String vars; + /** Whether the UEFI vars pflash is attached. */ + public final boolean varsEnabled; @NonNull public final String kernel; @NonNull @@ -73,12 +78,14 @@ public final class BootPlan { public final boolean entryFallback; private BootPlan( - boolean uefi, @NonNull String firmware, @NonNull String kernel, - @NonNull String initrd, @NonNull String cmdline, - @Nullable String entryTitle, boolean entryFallback + boolean uefi, @NonNull String firmware, @NonNull String vars, + boolean varsEnabled, @NonNull String kernel, @NonNull String initrd, + @NonNull String cmdline, @Nullable String entryTitle, boolean entryFallback ) { this.uefi = uefi; this.firmware = firmware; + this.vars = vars; + this.varsEnabled = varsEnabled; this.kernel = kernel; this.initrd = initrd; this.cmdline = cmdline; @@ -116,14 +123,18 @@ public static BootPlan resolve( if (BootConfig.BUILTIN_ENTRY_KEY.equals(entryOverrideId)) return resolveBuiltin(config, boot); if (boot.getProtocol() == BootConfig.Protocol.UEFI) - return new BootPlan(true, boot.getUefiFirmware(), "", "", "", null, false); + return new BootPlan( + true, boot.getUefiFirmware(), boot.getUefiVars(), + boot.isUefiVarsEnabled(), "", "", "", null, false + ); if (boot.getLinuxSource() == BootConfig.LinuxSource.MANUAL) { var kernel = boot.getKernel(); // pre-boot{} configs stored the EDK2 path as the kernel if (kernel.equals(PATH_EDK2_FIRMWARE)) - return new BootPlan(true, "", "", "", "", null, false); + return new BootPlan(true, "", "", false, "", "", "", null, false); return new BootPlan( - false, "", kernel, boot.getInitrd(), boot.getCmdline(), null, false + false, "", "", false, kernel, boot.getInitrd(), + boot.getCmdline(), null, false ); } return resolveImage(config, boot, entryOverrideId); @@ -142,7 +153,7 @@ public static BootPlan resolve( private static BootPlan resolveBuiltin( @NonNull VMConfig config, @NonNull BootConfig boot) { return new BootPlan( - false, "", PATH_BUILTIN_KERNEL, PATH_BUILTIN_INITRD, + false, "", "", false, PATH_BUILTIN_KERNEL, PATH_BUILTIN_INITRD, builtinCmdline(config, boot), "DroidVM built-in kernel", false ); } @@ -214,7 +225,7 @@ else if (pinned != null) var initrd = new File(cacheDir, "initrd"); var title = optStr(entry, "title"); return new BootPlan( - false, "", + false, "", "", false, new File(cacheDir, "kernel").getAbsolutePath(), initrd.exists() ? initrd.getAbsolutePath() : "", cmdline, diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackendInstance.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackendInstance.java index f38b1320b..af3050084 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackendInstance.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackendInstance.java @@ -4,6 +4,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; import static cn.classfun.droidvm.lib.Constants.DATA_DIR; import static cn.classfun.droidvm.lib.Constants.PATH_EDK2_FIRMWARE; +import static cn.classfun.droidvm.lib.Constants.PATH_EDK2_VARS; import static cn.classfun.droidvm.lib.store.enums.Enums.optEnum; import static cn.classfun.droidvm.lib.store.vm.DisplayBackend.SIMPLEFB; import static cn.classfun.droidvm.lib.store.vm.DisplayBackend.VIRTIO_GPU; @@ -230,12 +231,24 @@ private List buildCommand() { if (boot.uefi) { // crosvm has no custom-firmware support; always builtin EDK2 args.add(PATH_EDK2_FIRMWARE); + if (boot.varsEnabled) { + var vars = boot.vars.isEmpty() ? PATH_EDK2_VARS : boot.vars; + args.add("--pflash"); + args.add(fmt("path=%s,block_size=%d", vars, pflashBlockSize(vars))); + } } else if (!boot.kernel.isEmpty()) { args.add(boot.kernel); } return args; } + private static long pflashBlockSize(@NonNull String path) { + long size = new File(path).length(); + for (long blockSize : new long[]{262144, 65536, 4096}) + if (size > 0 && size % blockSize == 0) return blockSize; + return 262144; + } + private void buildDiskCommand(@NonNull List args) { var disks = config.item.opt("disks", null); if (disks == null) return; @@ -259,10 +272,6 @@ private void buildDiskCommand(@NonNull List args) { args.add("--pmem"); args.add(arg.toString()); break; - case PFLASH: - args.add("--pflash"); - args.add(arg.toString()); - break; case CDROM: args.add("--scsi-block"); arg.append(",ro=true,type=cdrom,lock=false"); diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/QemuBackendInstance.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/QemuBackendInstance.java index 5580c7461..c8e33c794 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/QemuBackendInstance.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/QemuBackendInstance.java @@ -4,6 +4,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; import static cn.classfun.droidvm.lib.Constants.DATA_DIR; import static cn.classfun.droidvm.lib.Constants.PATH_EDK2_QEMU_FIRMWARE; +import static cn.classfun.droidvm.lib.Constants.PATH_EDK2_VARS; import static cn.classfun.droidvm.lib.store.enums.Enums.optEnum; import static cn.classfun.droidvm.lib.utils.AssetUtils.getPrebuiltBinaryPath; import static cn.classfun.droidvm.lib.utils.FileUtils.deleteFile; @@ -190,6 +191,11 @@ private List buildCommand() { if (boot.uefi) { args.add("-bios"); args.add(boot.firmware.isEmpty() ? PATH_EDK2_QEMU_FIRMWARE : boot.firmware); + if (boot.varsEnabled) { + var vars = boot.vars.isEmpty() ? PATH_EDK2_VARS : boot.vars; + args.add("-drive"); + args.add(fmt("file=%s,if=pflash,format=raw", vars)); + } } if (!boot.kernel.isEmpty()) { args.add("-kernel"); diff --git a/app/src/main/java/cn/classfun/droidvm/lib/Constants.java b/app/src/main/java/cn/classfun/droidvm/lib/Constants.java index 5ce2353db..852e863f8 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/Constants.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/Constants.java @@ -18,6 +18,9 @@ public final class Constants { public static final String PATH_BUILTIN_INITRD = pathJoin(DATA_DIR, "/usr/share/droidvm/initramfs.img"); public static final String PATH_EDK2_FIRMWARE = pathJoin(DATA_DIR, "/usr/share/droidvm/edk2-gunyah.fd"); public static final String PATH_EDK2_QEMU_FIRMWARE = pathJoin(DATA_DIR, "/usr/share/droidvm/edk2-qemu.fd"); + public static final String PATH_EDK2_VARS = pathJoin(DATA_DIR, "/usr/share/droidvm/edk2-gunyah.vars.fd"); + /** crosvm arm64 pflash window: AARCH64_PLATFORM_MMIO_SIZE (8 MiB). */ + public static final long PFLASH_MAX_SIZE = 8L * 1024 * 1024; public static final String GITHUB_ISSUE_URL = "https://github.com/Droid-VM/DroidVM/issues"; // droidvm + daemon are CMake-built and shipped as APK assets. The // third-party daemons (gvswitch/pbridge/bridgedhcp/netbox) and lbx now diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/BootConfig.java b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/BootConfig.java index b15e03af6..97a0f551b 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/BootConfig.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/BootConfig.java @@ -17,7 +17,7 @@ *

      * boot: {
      *   protocol: "uefi" | "linux",
    - *   uefi:  { firmware },                       // empty = builtin EDK2
    + *   uefi:  { firmware, vars },                 // empty = builtin EDK2
      *   linux: {
      *     source: "manual" | "image",
      *     kernel, initrd, cmdline,                 // manual-source fields
    @@ -136,6 +136,25 @@ public void setUefiFirmware(@NonNull String firmware) {
             uefi().set("firmware", firmware);
         }
     
    +    /** Custom UEFI vars path; empty = builtin EDK2 vars. */
    +    @NonNull
    +    public String getUefiVars() {
    +        return uefi().optString("vars", "");
    +    }
    +
    +    public void setUefiVars(@NonNull String vars) {
    +        uefi().set("vars", vars);
    +    }
    +
    +    /** Whether the UEFI vars pflash is attached; off = no pflash. */
    +    public boolean isUefiVarsEnabled() {
    +        return uefi().optBoolean("vars_enabled", true);
    +    }
    +
    +    public void setUefiVarsEnabled(boolean enabled) {
    +        uefi().set("vars_enabled", enabled);
    +    }
    +
         // --- linux branch ---
     
         @NonNull
    diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/basic/VMEditBasicTab.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/basic/VMEditBasicTab.java
    index 4d5f4173b..31ce2dc2c 100644
    --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/basic/VMEditBasicTab.java
    +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/basic/VMEditBasicTab.java
    @@ -241,4 +241,11 @@ public void saveConfig(@NonNull VMConfig config) {
         public ProtectedVM getCurrentProtectedVm() {
             return chooseProtectedVm.getSelectedItem();
         }
    +
    +    /** The backend as currently selected */
    +    @NonNull
    +    public VMBackend getCurrentBackend() {
    +        var backend = chooseBackend.getSelectedItem();
    +        return backend == null ? VMBackend.DEFAULT : backend;
    +    }
     }
    diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/boot/VMEditBootTab.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/boot/VMEditBootTab.java
    index ace779d7b..0d3cbde55 100644
    --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/boot/VMEditBootTab.java
    +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/boot/VMEditBootTab.java
    @@ -2,6 +2,7 @@
     
     import static android.view.View.GONE;
     import static android.view.View.VISIBLE;
    +import static cn.classfun.droidvm.lib.Constants.PFLASH_MAX_SIZE;
     import static cn.classfun.droidvm.lib.Constants.PATH_BUILTIN_INITRD;
     import static cn.classfun.droidvm.lib.Constants.PATH_BUILTIN_KERNEL;
     import static cn.classfun.droidvm.lib.Constants.PATH_MICRODROID_INITRD;
    @@ -31,6 +32,7 @@
     import com.google.android.material.dialog.MaterialAlertDialogBuilder;
     import com.google.android.material.materialswitch.MaterialSwitch;
     
    +import java.io.File;
     import java.util.ArrayList;
     import java.util.regex.Pattern;
     
    @@ -38,6 +40,7 @@
     import cn.classfun.droidvm.lib.store.base.DataItem;
     import cn.classfun.droidvm.lib.store.vm.BootConfig;
     import cn.classfun.droidvm.lib.store.vm.ProtectedVM;
    +import cn.classfun.droidvm.lib.store.vm.VMBackend;
     import cn.classfun.droidvm.lib.store.vm.VMConfig;
     import cn.classfun.droidvm.lib.store.vm.VMStore;
     import cn.classfun.droidvm.lib.ui.CopyableField;
    @@ -63,6 +66,8 @@ public final class VMEditBootTab extends VMEditBaseTab {
         private DropdownRowWidget ddProtocol;
         private CollapsibleContainer containerUefi;
         private TextInputRowWidget inputUefiFirmware;
    +    private SwitchRowWidget swUefiVarsEnabled;
    +    private TextInputRowWidget inputUefiVars;
         private CollapsibleContainer containerKernel;
         private DropdownRowWidget ddSource;
         private View layoutImage;
    @@ -105,6 +110,8 @@ public void initView() {
             ddProtocol = view.findViewById(R.id.dd_boot_protocol);
             containerUefi = view.findViewById(R.id.container_uefi);
             inputUefiFirmware = view.findViewById(R.id.input_uefi_firmware);
    +        swUefiVarsEnabled = view.findViewById(R.id.sw_uefi_vars_enabled);
    +        inputUefiVars = view.findViewById(R.id.input_uefi_vars);
             containerKernel = view.findViewById(R.id.container_kernel);
             ddSource = view.findViewById(R.id.dd_kernel_source);
             layoutImage = view.findViewById(R.id.layout_kernel_image);
    @@ -157,6 +164,11 @@ public void initValue() {
                 if (isImageMode() && scanned == null) rescan();
             });
             inputUefiFirmware.setIconButtonOnClickListener(this::showFirmwareBrowseDialog);
    +        inputUefiVars.setIconButtonOnClickListener(this::showVarsBrowseDialog);
    +        swUefiVarsEnabled.setOnCheckedChangeListener((btn, checked) -> {
    +            inputUefiVars.setVisibility(checked ? VISIBLE : GONE);
    +            refreshStoragePflash();
    +        });
             inputKernel.setIconButtonOnClickListener(this::showKernelBrowseDialog);
             inputInitrd.setIconButtonOnClickListener(this::showInitrdBrowseDialog);
             btnRescan.setOnClickListener(v -> rescan());
    @@ -192,6 +204,30 @@ private boolean isImageMode() {
                 && source == BootConfig.LinuxSource.IMAGE;
         }
     
    +    /** Live value of the UEFI vars (pflash) enable switch. */
    +    public boolean isVarsEnabledLive() {
    +        return protocol == BootConfig.Protocol.UEFI
    +            && swUefiVarsEnabled != null && swUefiVarsEnabled.isChecked();
    +    }
    +
    +    /** Re-render the storage tab's disk list so PFLASH availability updates. */
    +    private void refreshStoragePflash() {
    +        try {
    +            var storage = (VMEditStorageTab) parent.getTab(VMEditTab.TAB_STORAGE);
    +            if (storage != null) storage.refreshDisks();
    +        } catch (Exception ignored) {
    +        }
    +    }
    +    /** The backend as currently selected in the basic tab (before save). */
    +    @NonNull
    +    private VMBackend currentBackend() {
    +        try {
    +            var basic = (VMEditBasicTab) parent.getTab(VMEditTab.TAB_BASIC);
    +            if (basic != null) return basic.getCurrentBackend();
    +        } catch (Exception ignored) {
    +        }
    +        return VMBackend.DEFAULT;
    +    }
         private void applyVisibility() {
             boolean uefi = protocol == BootConfig.Protocol.UEFI;
             containerUefi.setVisibility(uefi ? VISIBLE : GONE);
    @@ -199,6 +235,9 @@ private void applyVisibility() {
             boolean image = source == BootConfig.LinuxSource.IMAGE;
             layoutImage.setVisibility(image ? VISIBLE : GONE);
             layoutManual.setVisibility(image ? GONE : VISIBLE);
    +        if (uefi) {
    +            inputUefiVars.setVisibility(isVarsEnabledLive() ? VISIBLE : GONE);
    +        }
         }
     
         @Override
    @@ -210,6 +249,8 @@ public void loadConfig(@NonNull VMConfig config) {
             ddProtocol.setText(protocol.getDisplayString(parent));
             ddSource.setText(source.getDisplayString(parent));
             inputUefiFirmware.setTextAndMoveCursor(boot.getUefiFirmware());
    +        swUefiVarsEnabled.setChecked(boot.isUefiVarsEnabled());
    +        inputUefiVars.setTextAndMoveCursor(boot.getUefiVars());
             inputKernel.setTextAndMoveCursor(boot.getKernel());
             inputInitrd.setTextAndMoveCursor(boot.getInitrd());
             bootDiskIndex = boot.getImageDisk();
    @@ -234,6 +275,21 @@ public boolean validateInput(@NonNull VMStore store) {
                     inputUefiFirmware.setError(parent.getString(R.string.create_vm_error_path_invalid));
                     return false;
                 }
    +            var vars = inputUefiVars.getText();
    +            inputUefiVars.setError(null);
    +            if (isVarsEnabledLive() && !vars.isEmpty()) {
    +                if (!checkFilePath(vars, true)) {
    +                    inputUefiVars.setError(parent.getString(R.string.create_vm_error_path_invalid));
    +                    return false;
    +                }
    +                // crosvm-only pflash size limit
    +                if (currentBackend() == VMBackend.CROSVM
    +                    && new File(vars).length() > PFLASH_MAX_SIZE) {
    +                    inputUefiVars.setError(parent.getString(
    +                        R.string.edit_vm_uefi_vars_too_large, PFLASH_MAX_SIZE / (1024 * 1024)));
    +                    return false;
    +                }
    +            }
                 return true;
             }
             if (source == BootConfig.LinuxSource.IMAGE) {
    @@ -271,6 +327,8 @@ public void saveConfig(@NonNull VMConfig config) {
             var boot = BootConfig.of(config);
             boot.setProtocol(protocol);
             boot.setUefiFirmware(inputUefiFirmware.getText());
    +        boot.setUefiVarsEnabled(isVarsEnabledLive());
    +        boot.setUefiVars(inputUefiVars.getText());
             boot.setLinuxSource(source);
             boot.setKernel(inputKernel.getText());
             boot.setInitrd(inputInitrd.getText());
    @@ -588,6 +646,29 @@ private void showFirmwareBrowseDialog() {
             );
         }
     
    +    private void showVarsBrowseDialog() {
    +        MenuItem.OnMenuItemClickListener listener = item -> {
    +            var id = item.getItemId();
    +            if (id == R.id.menu_firmware_builtin) {
    +                inputUefiVars.setText("");
    +            } else if (id == R.id.menu_firmware_external) {
    +                parent.currentPicker = uri -> {
    +                    if (uri != null)
    +                        inputUefiVars.setTextAndMoveCursor(resolveUriPath(parent, uri));
    +                    parent.currentPicker = null;
    +                };
    +                parent.filePickerLauncher.launch(new String[]{"*/*"});
    +            }
    +            return true;
    +        };
    +        MenuDialogBuilder.showSimple(
    +            parent,
    +            R.string.edit_vm_uefi_vars_browse_title,
    +            R.menu.menu_vm_firmware_browse,
    +            listener
    +        );
    +    }
    +
         private void showKernelBrowseDialog() {
             MenuItem.OnMenuItemClickListener listener = item -> {
                 onKernelBrowseClick(item);
    diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/storage/VMEditStorageTab.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/storage/VMEditStorageTab.java
    index 9033e035c..5c516aa55 100644
    --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/storage/VMEditStorageTab.java
    +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/storage/VMEditStorageTab.java
    @@ -1,6 +1,7 @@
     package cn.classfun.droidvm.ui.vm.edit.storage;
     
     import static android.app.Activity.RESULT_OK;
    +import static cn.classfun.droidvm.lib.Constants.PFLASH_MAX_SIZE;
     import static cn.classfun.droidvm.lib.utils.FileUtils.checkFileName;
     import static cn.classfun.droidvm.lib.utils.FileUtils.checkFilePath;
     import static cn.classfun.droidvm.lib.utils.StringUtils.resolveUriPath;
    @@ -14,15 +15,22 @@
     import androidx.annotation.NonNull;
     import androidx.annotation.Nullable;
     
    +import java.io.File;
     import java.util.HashSet;
     
     import cn.classfun.droidvm.R;
     import cn.classfun.droidvm.lib.store.base.DataItem;
    +import cn.classfun.droidvm.lib.store.disk.DiskBus;
    +import cn.classfun.droidvm.lib.store.enums.Enums;
    +import cn.classfun.droidvm.lib.store.vm.VMBackend;
     import cn.classfun.droidvm.lib.store.vm.VMConfig;
     import cn.classfun.droidvm.lib.store.vm.VMStore;
     import cn.classfun.droidvm.ui.disk.action.DiskActionDialog;
     import cn.classfun.droidvm.ui.vm.edit.VMEditActivity;
     import cn.classfun.droidvm.ui.vm.edit.base.VMEditBaseTab;
    +import cn.classfun.droidvm.ui.vm.edit.base.VMEditTab;
    +import cn.classfun.droidvm.ui.vm.edit.basic.VMEditBasicTab;
    +import cn.classfun.droidvm.ui.vm.edit.boot.VMEditBootTab;
     import cn.classfun.droidvm.ui.vm.edit.storage.dir.VMSharedDirEditAdapter;
     import cn.classfun.droidvm.ui.vm.edit.storage.disk.VMDiskEditAdapter;
     import cn.classfun.droidvm.ui.widgets.container.CardItemListView;
    @@ -53,10 +61,24 @@ public void initValue() {
             diskAdapter = listDisks.setAdapter(VMDiskEditAdapter.class);
             diskAdapter.setOnBrowseFileListener(this::diskAdapterOnBrowseFile);
             diskAdapter.setOnImportOrCreateListener(this::diskAdapterOnImportOrCreate);
    +        // no PFLASH while the boot tab's UEFI vars pflash is enabled
    +        diskAdapter.setUefiVarsEnabledProvider(() -> {
    +            try {
    +                var boot = (VMEditBootTab) parent.getTab(VMEditTab.TAB_BOOT);
    +                return boot == null || boot.isVarsEnabledLive();
    +            } catch (Exception e) {
    +                return true;
    +            }
    +        });
             sharedDirAdapter = listSharedDirs.setAdapter(VMSharedDirEditAdapter.class);
             sharedDirAdapter.setOnBrowseListener(this::sharedDirAdapterOnBrowse);
         }
     
    +    // Refresh disk rows; PFLASH availability may have changed.
    +    public void refreshDisks() {
    +        if (diskAdapter != null) diskAdapter.notifyDataSetChanged();
    +    }
    +
         private void diskAdapterOnImportOrCreate(int pos) {
             Runnable onImportPickerUi = () -> parent.runOnUiThread(this::onImportPicker);
             pendingBrowsePosition = pos;
    @@ -128,9 +150,18 @@ public void loadConfig(@NonNull VMConfig config) {
     
         @Override
         public boolean validateInput(@NonNull VMStore store) {
    -        for (var disk : diskAdapter.getItems())
    -            if (!checkFilePath(disk.getValue().optString("path", ""), true))
    +        var crosvm = currentBackend() == VMBackend.CROSVM;
    +        for (var disk : diskAdapter.getItems()) {
    +            var d = disk.getValue();
    +            var path = d.optString("path", "");
    +            if (!checkFilePath(path, true))
                     return showValidateFailed(R.string.edit_vm_target_disk_path_invalid);
    +            // crosvm-only pflash size limit
    +            if (crosvm && Enums.optEnum(d, "bus", DiskBus.VIRTIO) == DiskBus.PFLASH
    +                && new File(path).length() > PFLASH_MAX_SIZE)
    +                return showValidateFailed(parent.getString(
    +                    R.string.edit_vm_uefi_vars_too_large, PFLASH_MAX_SIZE / (1024 * 1024)));
    +        }
             var set = new HashSet();
             for (var dir : sharedDirAdapter.getItems()) {
                 var d = dir.getValue();
    @@ -146,6 +177,17 @@ public boolean validateInput(@NonNull VMStore store) {
             return true;
         }
     
    +    /** The backend as currently selected in the basic tab (before save). */
    +    @NonNull
    +    private VMBackend currentBackend() {
    +        try {
    +            var basic = (VMEditBasicTab) parent.getTab(VMEditTab.TAB_BASIC);
    +            if (basic != null) return basic.getCurrentBackend();
    +        } catch (Exception ignored) {
    +        }
    +        return VMBackend.DEFAULT;
    +    }
    +
         @Override
         public void saveConfig(@NonNull VMConfig config) {
             config.item.set("disks", listDisks.getItems());
    diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/storage/disk/VMDiskEditAdapter.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/storage/disk/VMDiskEditAdapter.java
    index f39dd4102..c8271fdc6 100644
    --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/storage/disk/VMDiskEditAdapter.java
    +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/storage/disk/VMDiskEditAdapter.java
    @@ -16,6 +16,7 @@
     import com.google.android.material.dialog.MaterialAlertDialogBuilder;
     
     import cn.classfun.droidvm.R;
    +import cn.classfun.droidvm.lib.store.base.DataItem;
     import cn.classfun.droidvm.lib.store.disk.DiskBus;
     import cn.classfun.droidvm.lib.store.disk.DiskStore;
     import cn.classfun.droidvm.lib.store.enums.Enums;
    @@ -25,8 +26,14 @@
     import cn.classfun.droidvm.ui.widgets.container.CardItemAdapter;
     
     public final class VMDiskEditAdapter extends CardItemAdapter {
    +    @FunctionalInterface
    +    public interface UefiVarsEnabledProvider {
    +        boolean isEnabled();
    +    }
    +
         private OnItemClickListener browseFileListener;
         private OnItemClickListener importOrCreateListener;
    +    private UefiVarsEnabledProvider uefiVarsEnabledProvider = () -> true;
         private boolean readonlyChanged = false;
         private boolean updatingViews = false;
     
    @@ -47,6 +54,35 @@ public void setOnImportOrCreateListener(OnItemClickListener l) {
             this.importOrCreateListener = l;
         }
     
    +    public void setUefiVarsEnabledProvider(@NonNull UefiVarsEnabledProvider provider) {
    +        this.uefiVarsEnabledProvider = provider;
    +    }
    +
    +    private boolean pflashAllowed(int position) {
    +        if (uefiVarsEnabledProvider.isEnabled()) return false;
    +        for (int i = 0; i < items.size(); i++) {
    +            if (i == position) continue;
    +            var d = items.get(i);
    +            if (d.is(DataItem.Type.OBJECT)
    +                && Enums.optEnum(d, "bus", DiskBus.VIRTIO) == DiskBus.PFLASH)
    +                return false;
    +        }
    +        return true;
    +    }
    +
    +    @NonNull
    +    private DiskBus[] busOptions(int position) {
    +        if (!pflashAllowed(position))
    +            return new DiskBus[]{DiskBus.VIRTIO, DiskBus.SCSI, DiskBus.PMEM, DiskBus.CDROM};
    +        return new DiskBus[]{DiskBus.VIRTIO, DiskBus.SCSI, DiskBus.PMEM, DiskBus.CDROM, DiskBus.PFLASH};
    +    }
    +
    +    private static boolean containsPflash(@NonNull DiskBus[] options) {
    +        for (var bus : options)
    +            if (bus == DiskBus.PFLASH) return true;
    +        return false;
    +    }
    +
         public void setPathAt(int position, String path) {
             if (position < 0 || position >= items.size()) return;
             var disk = items.get(position);
    @@ -98,7 +134,15 @@ public void onBindViewHolder(@NonNull VMDiskEditViewHolder holder, int position)
             holder.unbindWatcher();
             holder.etPath.setText(disk.optString("path", ""));
             holder.switchReadonly.setChecked(disk.optBoolean("readonly", false));
    -        holder.btnBus.configure(DiskBus.class, Enums.optEnum(disk, "bus", DiskBus.VIRTIO));
    +        // unselectable PFLASH and fallback to VIRTIO.
    +        var bus = Enums.optEnum(disk, "bus", DiskBus.VIRTIO);
    +        var options = busOptions(position);
    +        if (bus == DiskBus.PFLASH && !containsPflash(options)) {
    +            bus = DiskBus.VIRTIO;
    +            if (!updatingViews) disk.set("bus", bus);
    +        }
    +        holder.btnBus.setItems(options);
    +        holder.btnBus.setSelectedItem(bus);
             holder.btnBus.setOnValueChangedListener((oldVal, newVal) -> {
                 if (updatingViews) return;
                 int pos = holder.getBindingAdapterPosition();
    @@ -108,7 +152,14 @@ public void onBindViewHolder(@NonNull VMDiskEditViewHolder holder, int position)
                 if (!readonlyChanged)
                     item.set("readonly", newVal == DiskBus.CDROM);
                 try {
    -                notifyItemChanged(pos);
    +                // PFLASH exclusivity changes other rows' options, so a
    +                // PFLASH switch needs a full refresh; any other
    +                // bus change only affects this row.
    +                if (oldVal == DiskBus.PFLASH || newVal == DiskBus.PFLASH) {
    +                    notifyDataSetChanged();
    +                } else {
    +                    notifyItemChanged(pos);
    +                }
                 } catch (Exception ignored) {
                 }
             });
    @@ -135,8 +186,14 @@ public void afterTextChanged(Editable s) {
             });
             holder.btnDelete.setOnClickListener(v -> {
                 int pos = holder.getBindingAdapterPosition();
    -            if (pos != RecyclerView.NO_POSITION)
    +            if (pos != RecyclerView.NO_POSITION) {
                     removeItem(pos);
    +                // re-enable PFLASH for the remaining rows if it was the only one
    +                try {
    +                    notifyDataSetChanged();
    +                } catch (Exception ignored) {
    +                }
    +            }
             });
         }
     
    diff --git a/app/src/main/res/layout/partial_vm_edit_boot.xml b/app/src/main/res/layout/partial_vm_edit_boot.xml
    index 47a7f6bef..016ba0f00 100644
    --- a/app/src/main/res/layout/partial_vm_edit_boot.xml
    +++ b/app/src/main/res/layout/partial_vm_edit_boot.xml
    @@ -36,6 +36,25 @@
                 app:ti_iconButtonHint="@string/edit_vm_disk_browse"
                 app:ti_iconButtonIcon="@drawable/ic_open_in" />
     
    +        
    +
    +        
    +
             内置 EDK2
         选择 UEFI 固件
         自定义固件仅对 QEMU 后端生效\ncrosvm 始终使用内置 EDK2\nUEFI 引导 Linux 适配中,尚不可用;如需引导 Linux,请选择 Linux 内核引导方式
    +    UEFI 变量(留空 = 内置 EDK2 变量)
    +    选择 UEFI 变量文件
    +    对于 crosvm,pflash 镜像大小不能超过 %1$d MiB
    +    启用 UEFI 变量(pflash)
    +    挂载 UEFI 变量存储。启用后,存储分区中不可再选择 PFLASH 总线。
         内核来源
         磁盘镜像
         手动指定
    diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
    index 39c94825e..8e6e89cf6 100644
    --- a/app/src/main/res/values-zh-rTW/strings.xml
    +++ b/app/src/main/res/values-zh-rTW/strings.xml
    @@ -298,6 +298,11 @@
         內建 EDK2
         選擇 UEFI 韌體
         自定義韌體僅對 QEMU 後端生效\ncrosvm 始終使用內建 EDK2\nUEFI 引導 Linux 適配中,尚不可用;如需引導 Linux,請選擇 Linux 核心引導方式
    +    UEFI 變數(留空 = 內建 EDK2 變數)
    +    選擇 UEFI 變數檔案
    +    對於 crosvm,pflash 鏡像大小不能超過 %1$d MiB
    +    啟用 UEFI 變數(pflash)
    +    掛載 UEFI 變數儲存。啟用後,儲存區中不可再選擇 PFLASH 匯流排。
         核心來源
         磁碟映像
         手動指定
    diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
    index 4532fc13b..3b406d53e 100644
    --- a/app/src/main/res/values/strings.xml
    +++ b/app/src/main/res/values/strings.xml
    @@ -300,6 +300,11 @@
         Built-in EDK2
         Select UEFI firmware
         Custom firmware only takes effect on the QEMU backend\ncrosvm always uses the built-in EDK2\nBooting Linux via UEFI is still being adapted and is not usable yet -- to boot Linux, choose the Linux kernel boot protocol
    +    UEFI vars (empty = built-in EDK2 vars)
    +    Select UEFI vars
    +    For crosvm, pflash image size must not exceed %1$d MiB
    +    Enable UEFI vars (pflash)
    +    Attach the UEFI variable store. While enabled, the PFLASH bus is unavailable in the storage section.
         Kernel source
         Disk image
         Manual
    
    From 0f2393704038664ed493afcbb55a8e188eb70ceb Mon Sep 17 00:00:00 2001
    From: HuJK 
    Date: Sun, 30 Aug 2026 13:37:07 +0800
    Subject: [PATCH 126/182] licensing: say the GPLv3 out loud, and allow an
     upstream relicense
    
    LICENSE.txt has always been the full GPLv3, but not one source file said so:
    397 Java files with no header at all, so the licence of a file could only be
    inferred from the repository around it. Every source file now carries an
    SPDX-License-Identifier line.
    
    ADDITIONAL-PERMISSIONS is the part that is new rather than merely written
    down. It lets anyone relicense this material under the terms an upstream
    project requires, for the purpose of getting it merged there and only for
    that -- which is what these changes exist for, and what a plain GPLv3 file
    cannot do when the project it is aimed at is Apache-2.0 or BSD. Contributions
    carry the permission because they are made under the project licence;
    CONTRIBUTING.md asks for a DCO sign-off rather than a CLA, so nobody signs
    away anything to hold it.
    
    LICENSE.txt itself is left alone. It is reflowed rather than hard-wrapped the
    way the FSF renders it, which is a cosmetic difference in a file that should
    not be churned.
    ---
     ADDITIONAL-PERMISSIONS                        |  61 ++
     CONTRIBUTING.md                               |  32 +
     LICENSING.md                                  |  31 +
     .../java/cn/classfun/droidvm/DroidVMApp.java  |   3 +
     .../cn/classfun/droidvm/daemon/Daemon.java    |   3 +
     .../droidvm/daemon/console/ConsoleStream.java |   3 +
     .../daemon/console/FDPipeConsoleStream.java   |   3 +
     .../daemon/console/FDSocketConsoleStream.java |   3 +
     .../daemon/console/InputConsoleStream.java    |   3 +
     .../console/LocalSocketConsoleStream.java     |   3 +
     .../daemon/console/SimpleConsoleStream.java   |   3 +
     .../daemon/display/DaemonSystemContext.java   |   3 +
     .../daemon/display/NativeDisplayBinder.java   |   3 +
     .../droidvm/daemon/ipc/basic/AuthHandler.java |   3 +
     .../droidvm/daemon/ipc/basic/PingHandler.java |   3 +
     .../daemon/ipc/basic/SetAppConfig.java        |   3 +
     .../daemon/ipc/basic/VersionHandler.java      |   3 +
     .../daemon/ipc/network/AddAddressHandler.java |   3 +
     .../ipc/network/AddInterfaceHandler.java      |   3 +
     .../daemon/ipc/network/CreateHandler.java     |   3 +
     .../daemon/ipc/network/DeleteHandler.java     |   3 +
     .../daemon/ipc/network/ExistsHandler.java     |   3 +
     .../daemon/ipc/network/InfoHandler.java       |   3 +
     .../daemon/ipc/network/ListHandler.java       |   3 +
     .../ipc/network/ListInterfacesHandler.java    |   3 +
     .../ipc/network/ListUplinksHandler.java       |   3 +
     .../daemon/ipc/network/ModifyHandler.java     |   3 +
     .../daemon/ipc/network/PdReleaseHandler.java  |   3 +
     .../daemon/ipc/network/PdRenewHandler.java    |   3 +
     .../ipc/network/RemoveAddressHandler.java     |   3 +
     .../ipc/network/RemoveInterfaceHandler.java   |   3 +
     .../daemon/ipc/network/StartHandler.java      |   3 +
     .../daemon/ipc/network/StatusHandler.java     |   3 +
     .../daemon/ipc/network/StopHandler.java       |   3 +
     .../daemon/ipc/network/ToolLogHandler.java    |   3 +
     .../daemon/ipc/vm/BootScanHandler.java        |   3 +
     .../daemon/ipc/vm/ConsoleClearHandler.java    |   3 +
     .../daemon/ipc/vm/ConsoleHistoryHandler.java  |   3 +
     .../daemon/ipc/vm/ConsoleInfoHandler.java     |   3 +
     .../daemon/ipc/vm/ConsoleListHandler.java     |   3 +
     .../daemon/ipc/vm/ConsoleWriteHandler.java    |   3 +
     .../droidvm/daemon/ipc/vm/ControlHandler.java |   3 +
     .../droidvm/daemon/ipc/vm/CreateHandler.java  |   3 +
     .../droidvm/daemon/ipc/vm/DeleteHandler.java  |   3 +
     .../daemon/ipc/vm/DisplayAttachHandler.java   |   3 +
     .../droidvm/daemon/ipc/vm/ExistsHandler.java  |   3 +
     .../droidvm/daemon/ipc/vm/ExportHandler.java  |   3 +
     .../droidvm/daemon/ipc/vm/GetHandler.java     |   3 +
     .../droidvm/daemon/ipc/vm/ImportHandler.java  |   3 +
     .../droidvm/daemon/ipc/vm/InputHandler.java   |   3 +
     .../droidvm/daemon/ipc/vm/ListHandler.java    |   3 +
     .../droidvm/daemon/ipc/vm/ModifyHandler.java  |   3 +
     .../droidvm/daemon/ipc/vm/RebootHandler.java  |   3 +
     .../droidvm/daemon/ipc/vm/ResumeHandler.java  |   3 +
     .../droidvm/daemon/ipc/vm/StartHandler.java   |   3 +
     .../droidvm/daemon/ipc/vm/StatusHandler.java  |   3 +
     .../droidvm/daemon/ipc/vm/StopAllHandler.java |   3 +
     .../droidvm/daemon/ipc/vm/StopHandler.java    |   3 +
     .../droidvm/daemon/ipc/vm/SuspendHandler.java |   3 +
     .../droidvm/daemon/ipc/vm/VncInfoHandler.java |   3 +
     .../daemon/network/NetworkInstance.java       |   3 +
     .../daemon/network/NetworkInstanceStore.java  |   3 +
     .../daemon/network/NetworkWatchdog.java       |   3 +
     .../daemon/network/backend/BackendBase.java   |   3 +
     .../daemon/network/backend/BridgeBackend.java |   3 +
     .../daemon/network/backend/BridgeDhcp.java    |   3 +
     .../network/backend/DefaultRouterWatcher.java |   3 +
     .../network/backend/FirewallHelper.java       |   3 +
     .../network/backend/LinuxBridgeBackend.java   |   3 +
     .../daemon/network/backend/LinuxNetwork.java  |   3 +
     .../network/backend/ManagedProcess.java       |   3 +
     .../daemon/network/backend/Netbox.java        |   3 +
     .../daemon/network/backend/Pbridge.java       |   3 +
     .../network/backend/UplinkResolver.java       |   3 +
     .../backend/gvisor/GvisorBridgeBackend.java   |   3 +
     .../backend/gvisor/GvswitchClient.java        |   3 +
     .../backend/gvisor/GvswitchConfigBuilder.java |   3 +
     .../network/backend/iptables/ChainInfo.java   |   3 +
     .../backend/iptables/IptablesBackend.java     |   3 +
     .../iptables/IptablesNetworkInstance.java     |   3 +
     .../daemon/network/backend/pd/Duid.java       |   3 +
     .../droidvm/daemon/server/ClientHandler.java  |   3 +
     .../droidvm/daemon/server/ClientRequest.java  |   3 +
     .../droidvm/daemon/server/ClientResponse.java |   3 +
     .../daemon/server/RequestException.java       |   3 +
     .../droidvm/daemon/server/RequestHandler.java |   3 +
     .../daemon/server/RequestHandlerStore.java    |   3 +
     .../droidvm/daemon/server/Server.java         |   3 +
     .../droidvm/daemon/server/ServerContext.java  |   3 +
     .../classfun/droidvm/daemon/vm/BootPlan.java  |   3 +
     .../droidvm/daemon/vm/SerialPipe.java         |   3 +
     .../droidvm/daemon/vm/VMBackendInstance.java  |   3 +
     .../droidvm/daemon/vm/VMInstance.java         |   3 +
     .../droidvm/daemon/vm/VMInstanceStore.java    |   3 +
     .../droidvm/daemon/vm/VMStartResult.java      |   3 +
     .../daemon/vm/backend/BackendBase.java        |   3 +
     .../daemon/vm/backend/CrosvmBackend.java      |   3 +
     .../vm/backend/CrosvmBackendInstance.java     |   3 +
     .../vm/backend/NativeDisplayInputBridge.java  |   3 +
     .../daemon/vm/backend/QemuBackend.java        |   3 +
     .../vm/backend/QemuBackendInstance.java       | 186 +++-
     .../droidvm/daemon/vm/pkg/VMExportTask.java   |   3 +
     .../droidvm/daemon/vm/pkg/VMExportUtils.java  |   3 +
     .../droidvm/daemon/vm/pkg/VMImportTask.java   |   3 +
     .../droidvm/daemon/vm/pkg/VMImportUtils.java  |   3 +
     .../cn/classfun/droidvm/lib/Constants.java    |   3 +
     .../cn/classfun/droidvm/lib/api/ApiInfo.java  |   3 +
     .../classfun/droidvm/lib/api/ApiManager.java  |   3 +
     .../droidvm/lib/api/ApiServiceInfo.java       |   3 +
     .../cn/classfun/droidvm/lib/api/Privacy.java  |   3 +
     .../droidvm/lib/archive/Compression.java      |   3 +
     .../lib/archive/LimitedInputStream.java       |   3 +
     .../lib/archive/OutputStreamLimiter.java      |   3 +
     .../archive/RandomAccessFileOutputStream.java |   3 +
     .../droidvm/lib/archive/TarReader.java        |   3 +
     .../droidvm/lib/archive/TarWriter.java        |   3 +
     .../classfun/droidvm/lib/crypt/HashFile.java  |   3 +
     .../classfun/droidvm/lib/crypt/HashItem.java  |   3 +
     .../droidvm/lib/daemon/DaemonClient.java      |   3 +
     .../droidvm/lib/daemon/DaemonConnection.java  |   3 +
     .../droidvm/lib/daemon/DaemonHelper.java      |   3 +
     .../lib/daemon/ForegroundCallback.java        |   3 +
     .../droidvm/lib/daemon/PendingRequest.java    |   3 +
     .../classfun/droidvm/lib/daemon/Protocol.java |   3 +
     .../droidvm/lib/daemon/RequestContext.java    |   3 +
     .../droidvm/lib/daemon/VMEventHandler.java    |   3 +
     .../classfun/droidvm/lib/data/CrosvmExit.java |   3 +
     .../cn/classfun/droidvm/lib/data/Images.java  |   3 +
     .../classfun/droidvm/lib/data/Language.java   |   3 +
     .../cn/classfun/droidvm/lib/data/License.java |   3 +
     .../droidvm/lib/data/QcomChipName.java        |   3 +
     .../droidvm/lib/data/QcomGunyahSupports.java  |   3 +
     .../cn/classfun/droidvm/lib/data/Repos.java   |   3 +
     .../classfun/droidvm/lib/diag/LogHelper.java  |   3 +
     .../droidvm/lib/diag/LogHelperHandler.java    |   3 +
     .../handler/BadSM8650HostKernelHandler.java   |   3 +
     .../diag/handler/HugePageFaultHandler.java    |   3 +
     .../OsKernelWithoutRestrictPoolHandler.java   |   3 +
     .../UnsupportedGunyahVersionHandler.java      |   3 +
     .../handler/UnsupportedSandboxHandler.java    |   3 +
     .../lib/download/DiskDownloadManager.java     |   3 +
     .../lib/download/DiskDownloadService.java     |   3 +
     .../droidvm/lib/natives/NativeProcess.java    |   3 +
     .../droidvm/lib/natives/UnixHelper.java       |   3 +
     .../droidvm/lib/network/FDSocket.java         |   3 +
     .../droidvm/lib/network/IPAddress.java        |   3 +
     .../droidvm/lib/network/IPNetwork.java        |   3 +
     .../droidvm/lib/network/IPv4Address.java      |   3 +
     .../droidvm/lib/network/IPv4Network.java      |   3 +
     .../droidvm/lib/network/IPv6Address.java      |   3 +
     .../droidvm/lib/network/IPv6Network.java      |   3 +
     .../cn/classfun/droidvm/lib/pkg/BootFile.java |   3 +
     .../classfun/droidvm/lib/pkg/DiskEntry.java   |   3 +
     .../cn/classfun/droidvm/lib/pkg/DiskRef.java  |   3 +
     .../droidvm/lib/pkg/PackageConstants.java     |   3 +
     .../droidvm/lib/pkg/PackageHeader.java        |   3 +
     .../droidvm/lib/pkg/PackageInput.java         |   3 +
     .../droidvm/lib/pkg/PackageManifest.java      |   3 +
     .../cn/classfun/droidvm/lib/pkg/Phase.java    |   3 +
     .../classfun/droidvm/lib/run/RunContext.java  |   3 +
     .../classfun/droidvm/lib/run/RunResult.java   |   3 +
     .../droidvm/lib/run/root/RootRunContext.java  |   3 +
     .../droidvm/lib/run/root/RootRunResult.java   |   3 +
     .../lib/run/system/SystemRunContext.java      |   3 +
     .../lib/run/system/SystemRunResult.java       |   3 +
     .../classfun/droidvm/lib/size/SizeNumber.java |   3 +
     .../classfun/droidvm/lib/size/SizeUnit.java   |   3 +
     .../classfun/droidvm/lib/size/SizeUtils.java  |   3 +
     .../droidvm/lib/store/base/DataConfig.java    |   3 +
     .../droidvm/lib/store/base/DataItem.java      |   3 +
     .../droidvm/lib/store/base/DataStore.java     |   3 +
     .../droidvm/lib/store/base/JSONSerialize.java |   3 +
     .../droidvm/lib/store/base/RingBuffer.java    |   3 +
     .../droidvm/lib/store/disk/DiskBus.java       |   3 +
     .../droidvm/lib/store/disk/DiskConfig.java    |   3 +
     .../droidvm/lib/store/disk/DiskStore.java     |   3 +
     .../droidvm/lib/store/enums/ColorEnum.java    |   3 +
     .../droidvm/lib/store/enums/EnumPicker.java   |   3 +
     .../droidvm/lib/store/enums/Enums.java        |   3 +
     .../droidvm/lib/store/enums/StringEnum.java   |   3 +
     .../droidvm/lib/store/network/BridgeType.java |   3 +
     .../droidvm/lib/store/network/Ipv6Source.java |   3 +
     .../lib/store/network/NetworkConfig.java      |   3 +
     .../store/network/NetworkConfigValidator.java |   3 +
     .../lib/store/network/NetworkState.java       |   3 +
     .../lib/store/network/NetworkStore.java       |   3 +
     .../droidvm/lib/store/network/UplinkMode.java |   3 +
     .../droidvm/lib/store/network/VlanConfig.java |   3 +
     .../droidvm/lib/store/vm/BootConfig.java      |   3 +
     .../droidvm/lib/store/vm/DisplayBackend.java  |   3 +
     .../classfun/droidvm/lib/store/vm/GpuApi.java |   3 +
     .../droidvm/lib/store/vm/GpuBackend.java      |   3 +
     .../droidvm/lib/store/vm/LendMthpMode.java    |   3 +
     .../droidvm/lib/store/vm/NativeDisplay.java   |   3 +
     .../droidvm/lib/store/vm/PortProtocol.java    |   3 +
     .../droidvm/lib/store/vm/ProtectedVM.java     |   3 +
     .../droidvm/lib/store/vm/SharedDirCache.java  |   3 +
     .../droidvm/lib/store/vm/SharedDirType.java   |   3 +
     .../droidvm/lib/store/vm/VMBackend.java       |   3 +
     .../droidvm/lib/store/vm/VMConfig.java        | 110 ++
     .../droidvm/lib/store/vm/VMHypervisor.java    |   3 +
     .../droidvm/lib/store/vm/VMNicConfig.java     |   3 +
     .../droidvm/lib/store/vm/VMState.java         |   3 +
     .../droidvm/lib/store/vm/VMStore.java         |   3 +
     .../droidvm/lib/ui/BackAskHelper.java         |   3 +
     .../droidvm/lib/ui/CopyableField.java         |   3 +
     .../droidvm/lib/ui/DragTouchListener.java     |   3 +
     .../droidvm/lib/ui/IconItemAdapter.java       |   3 +
     .../droidvm/lib/ui/ImeInsetsApplier.java      |   3 +
     .../droidvm/lib/ui/ImeInsetsExempt.java       |   3 +
     .../classfun/droidvm/lib/ui/MaterialMenu.java |   3 +
     .../droidvm/lib/ui/MenuDialogBuilder.java     |   3 +
     .../lib/ui/NotificationPermission.java        |   3 +
     .../lib/ui/SimpleAdapterDataObserver.java     |   3 +
     .../droidvm/lib/ui/SimpleTextWatcher.java     |   3 +
     .../droidvm/lib/ui/SwipeableTabActivity.java  |   3 +
     .../droidvm/lib/ui/TabSwipeHelper.java        |   3 +
     .../cn/classfun/droidvm/lib/ui/UIContext.java |   3 +
     .../termux/SimpleTerminalSessionClient.java   |   3 +
     .../ui/termux/SimpleTerminalViewClient.java   |   3 +
     .../droidvm/lib/ui/termux/TerminalFonts.java  |   3 +
     .../droidvm/lib/utils/AssetUtils.java         |  23 +
     .../droidvm/lib/utils/BinaryUtils.java        |   3 +
     .../classfun/droidvm/lib/utils/CpuUtils.java  |   3 +
     .../classfun/droidvm/lib/utils/FileUtils.java |   3 +
     .../droidvm/lib/utils/ImageUtils.java         |   3 +
     .../classfun/droidvm/lib/utils/JsonUtils.java |   3 +
     .../classfun/droidvm/lib/utils/NetUtils.java  |   3 +
     .../droidvm/lib/utils/ProcessUtils.java       |   3 +
     .../classfun/droidvm/lib/utils/RunUtils.java  |   3 +
     .../droidvm/lib/utils/ShareUtils.java         |   3 +
     .../droidvm/lib/utils/StringUtils.java        |   3 +
     .../droidvm/lib/utils/ThreadUtils.java        |   3 +
     .../cn/classfun/droidvm/lib/utils/Try.java    |   3 +
     .../classfun/droidvm/ui/SplashActivity.java   |   3 +
     .../ui/agent/AgentOperationActivity.java      |   3 +
     .../droidvm/ui/agent/base/AgentVM.java        |   3 +
     .../droidvm/ui/agent/base/BaseAction.java     |   3 +
     .../password/ChangePasswordActivity.java      |   3 +
     .../ui/agent/password/PasswordAction.java     |   3 +
     .../ui/disk/action/DiskActionDialog.java      |   3 +
     .../ui/disk/action/DiskCloneDialog.java       |   3 +
     .../ui/disk/action/DiskResizeDialog.java      |   3 +
     .../ui/disk/action/DiskSetFormatDialog.java   |   3 +
     .../droidvm/ui/disk/create/DiskCompress.java  |   3 +
     .../ui/disk/create/DiskCreateActivity.java    |   3 +
     .../droidvm/ui/disk/create/DiskFormat.java    |   3 +
     .../ui/disk/download/ImportURLActivity.java   |   3 +
     .../droidvm/ui/disk/images/FlatImage.java     |   3 +
     .../droidvm/ui/disk/images/ImageAdapter.java  |   3 +
     .../ui/disk/images/ImagePickerDialog.java     |   3 +
     .../ui/disk/images/ImageViewHolder.java       |   3 +
     .../ui/disk/images/ImportImagesActivity.java  |   3 +
     .../ui/disk/info/DiskInfoActivity.java        |   3 +
     .../ui/disk/info/base/DiskInfoBaseTab.java    |   3 +
     .../ui/disk/info/base/DiskInfoTab.java        |   3 +
     .../ui/disk/info/info/DiskInfoInfoTab.java    |   3 +
     .../info/snapshot/DiskInfoSnapshotTab.java    |   3 +
     .../snapshot/DiskSnapshotEntryAdapter.java    |   3 +
     .../snapshot/DiskSnapshotEntryViewHolder.java |   3 +
     .../ui/disk/info/tree/DiskInfoTreeTab.java    |   3 +
     .../disk/info/tree/DiskTreeEntryAdapter.java  |   3 +
     .../info/tree/DiskTreeEntryViewHolder.java    |   3 +
     .../ui/disk/lxc/ImportLxcImagesActivity.java  |   3 +
     .../droidvm/ui/disk/lxc/LxcImage.java         |   3 +
     .../droidvm/ui/disk/lxc/LxcImageParser.java   |   3 +
     .../disk/operation/DiskOperationActivity.java |   3 +
     .../disk/operation/ImageCommandGenerate.java  |   3 +
     .../ui/hugepage/AcquireContainerView.java     |   3 +
     .../droidvm/ui/hugepage/HugePageActivity.java |   3 +
     .../droidvm/ui/hugepage/HugePageColor.java    |   3 +
     .../droidvm/ui/hugepage/HugePageModel.java    |   3 +
     .../droidvm/ui/hugepage/HugePageProcess.java  |   3 +
     .../ui/hugepage/HugePageProcessActivity.java  |   3 +
     .../ui/hugepage/HugePageProcessAdapter.java   |   3 +
     .../droidvm/ui/hugepage/SegmentedBar.java     |   3 +
     .../classfun/droidvm/ui/logs/LogAdapter.java  |   3 +
     .../droidvm/ui/logs/LogViewHolder.java        |   3 +
     .../droidvm/ui/logs/LogsActivity.java         |   3 +
     .../droidvm/ui/main/MainActivity.java         |   3 +
     .../droidvm/ui/main/base/BaseViewHolder.java  |   3 +
     .../ui/main/base/MainBaseFragment.java        |   3 +
     .../ui/main/base/MainFragmentEnum.java        |   3 +
     .../ui/main/base/list/DataAdapter.java        |   3 +
     .../ui/main/base/list/MainListFragment.java   |   3 +
     .../base/stateful/MainStatefulFragment.java   |   3 +
     .../main/base/stateful/StatefulAdapter.java   |   3 +
     .../droidvm/ui/main/disk/DiskAdapter.java     |   3 +
     .../droidvm/ui/main/disk/ImageInfo.java       |   3 +
     .../ui/main/disk/MainDiskFragment.java        |   3 +
     .../ui/main/home/MainHomeFragment.java        |   3 +
     .../ui/main/network/MainNetworkFragment.java  |   3 +
     .../ui/main/network/NetworkAdapter.java       |   3 +
     .../ui/main/settings/ApiManagerDialog.java    |   3 +
     .../ui/main/settings/ApiServiceAdapter.java   |   3 +
     .../main/settings/ApiServiceViewHolder.java   |   3 +
     .../ui/main/settings/LicenseListAdapter.java  |   3 +
     .../main/settings/MainSettingsFragment.java   |   3 +
     .../main/settings/SoftwareLicenseDialog.java  |   3 +
     .../droidvm/ui/main/vm/MainVMFragment.java    |   3 +
     .../droidvm/ui/main/vm/VMAdapter.java         |   3 +
     .../droidvm/ui/network/NetworkActions.java    |   3 +
     .../ui/network/edit/NetworkEditActivity.java  |   3 +
     .../ui/network/edit/VlanCardBinder.java       |   3 +
     .../ui/network/info/NetworkInfoActivity.java  |   3 +
     .../network/info/NetworkToolLogActivity.java  |   3 +
     .../droidvm/ui/setup/SetupActivity.java       |   3 +
     .../ui/setup/base/BaseCheckStepFragment.java  |  19 +
     .../ui/setup/base/BaseStepFragment.java       |   3 +
     .../ui/setup/step/DoneStepFragment.java       |   3 +
     .../ui/setup/step/ExtractStepFragment.java    |   3 +
     .../ui/setup/step/PrivacyStepFragment.java    |   3 +
     .../ui/setup/step/RootStepFragment.java       |   3 +
     .../ui/setup/step/SocStepFragment.java        |   3 +
     .../ui/setup/step/StartStepFragment.java      |   3 +
     .../ui/setup/step/StorageStepFragment.java    |   3 +
     .../step/VirtualizationStepFragment.java      |   3 +
     .../droidvm/ui/update/UpdateDialog.java       |   3 +
     .../droidvm/ui/update/UpdateInfo.java         |   3 +
     .../droidvm/ui/update/VersionCheck.java       |   3 +
     .../droidvm/ui/vm/NicLeaseAllocator.java      |   3 +
     .../cn/classfun/droidvm/ui/vm/VMActions.java  |   3 +
     .../droidvm/ui/vm/boot/BootEntries.java       |   3 +
     .../droidvm/ui/vm/boot/BootMenuDialog.java    |   3 +
     .../ui/vm/console/VMConsoleActivity.java      |   3 +
     .../ui/vm/console/VMConsoleRouter.java        |   3 +
     .../vm/display/base/BaseExtraKeysAdapter.java |   3 +
     .../display/base/DisplayExtraKeysPanel.java   |   3 +
     .../vm/display/base/DisplayPresentation.java  |   3 +
     .../vm/display/base/DisplayTouchPadPanel.java |   3 +
     .../ui/vm/display/base/KeyListener.java       |   3 +
     .../ui/vm/display/base/UsbHidInput.java       |   3 +
     .../droidvm/ui/vm/display/base/X11Keymap.java |   3 +
     .../display/DisplayProvider.java              |   3 +
     .../display/VMNativeDisplayActivity.java      |   3 +
     .../nativedisplay/input/DirectInputSink.java  |   3 +
     .../nativedisplay/input/EvdevEncoder.java     |   3 +
     .../nativedisplay/input/InputForwarder.java   |   3 +
     .../nativedisplay/input/KeyCodeMapper.java    |   3 +
     .../input/NativeExtraKeysPanel.java           |   3 +
     .../input/NativeKeyboardEditText.java         |   3 +
     .../input/TouchScaleCalculator.java           |   3 +
     .../vm/display/vnc/base/BaseVncActivity.java  |   3 +
     .../ui/vm/display/vnc/base/VncClient.java     |   3 +
     .../vm/display/vnc/base/VncDisplayView.java   |   3 +
     .../vnc/display/VMVncDisplayActivity.java     |   3 +
     .../display/VMVncPresentationActivity.java    |   3 +
     .../display/vnc/input/VncExtraKeysPanel.java  |   3 +
     .../display/vnc/input/VncTouchPadPanel.java   |   3 +
     .../droidvm/ui/vm/edit/VMEditActivity.java    |  49 +-
     .../ui/vm/edit/base/VMEditBaseTab.java        |   3 +
     .../droidvm/ui/vm/edit/base/VMEditTab.java    |   3 +
     .../ui/vm/edit/basic/VMEditBasicTab.java      | 292 +++++-
     .../ui/vm/edit/boot/VMEditBootTab.java        |   3 +
     .../vm/edit/graphics/VMEditGraphicsTab.java   | 980 +++++++++++++++---
     .../ui/vm/edit/network/VMEditNetworkTab.java  |   3 +
     .../ui/vm/edit/network/VMNetEditAdapter.java  |   3 +
     .../vm/edit/network/VMNetEditViewHolder.java  |   3 +
     .../ui/vm/edit/storage/VMEditStorageTab.java  |   3 +
     .../storage/dir/VMSharedDirEditAdapter.java   |   3 +
     .../dir/VMSharedDirEditViewHolder.java        |   3 +
     .../edit/storage/disk/VMDiskEditAdapter.java  |   3 +
     .../storage/disk/VMDiskEditViewHolder.java    |   3 +
     .../droidvm/ui/vm/info/ConsoleButton.java     |   3 +
     .../droidvm/ui/vm/info/VMInfoActivity.java    |   3 +
     .../vm/pkg/exports/VMPkgExportActivity.java   |   3 +
     .../pkg/exports/VMPkgExportDiskAdapter.java   |   3 +
     .../ui/vm/pkg/imports/NetworkImportMode.java  |   3 +
     .../vm/pkg/imports/VMPkgImportActivity.java   |   3 +
     .../pkg/imports/VMPkgImportDiskAdapter.java   |   3 +
     .../imports/VMPkgImportNetworkAdapter.java    |   3 +
     .../ui/widgets/container/CardItemAdapter.java |   3 +
     .../widgets/container/CardItemListView.java   |   3 +
     .../container/CollapsibleContainer.java       |   3 +
     .../ui/widgets/row/ChooseRowWidget.java       |   3 +
     .../ui/widgets/row/DropdownRowWidget.java     |   3 +
     .../ui/widgets/row/SwitchRowWidget.java       |   3 +
     .../ui/widgets/row/TextInputRowWidget.java    |   3 +
     .../droidvm/ui/widgets/row/TextRowWidget.java |   3 +
     .../ui/widgets/tools/DownloadWidget.java      |   3 +
     .../widgets/tools/KernelAnalysisWidget.java   |   3 +
     .../ui/widgets/tools/PickerButtonWidget.java  |   3 +
     .../res/layout/partial_vm_edit_graphics.xml   | 674 +++++++-----
     383 files changed, 3112 insertions(+), 461 deletions(-)
     create mode 100644 ADDITIONAL-PERMISSIONS
     create mode 100644 CONTRIBUTING.md
     create mode 100644 LICENSING.md
    
    diff --git a/ADDITIONAL-PERMISSIONS b/ADDITIONAL-PERMISSIONS
    new file mode 100644
    index 000000000..987ba2602
    --- /dev/null
    +++ b/ADDITIONAL-PERMISSIONS
    @@ -0,0 +1,61 @@
    +Additional permissions under GNU GPL version 3, section 7
    +=========================================================
    +
    +These additional permissions apply to the parts of this work copyrighted by
    +the Droid-VM organization and by contributors to this project. They do not
    +apply to material copyrighted by others -- in particular, material inherited
    +from the upstream project this repository is derived from, which remains under
    +its own terms and is not affected by anything below.
    +
    +
    +1. Upstream contribution
    +------------------------
    +
    +You may modify this material and distribute the result under the license terms
    +that an Upstream Project requires of contributions to it, for the sole purpose
    +of having that material included in the Upstream Project's official repository.
    +
    +This permission takes effect only for material that the Upstream Project
    +accepts and publishes. Proposing material to an Upstream Project -- opening a
    +merge request, posting a patch, or any other act of submission -- does not by
    +itself place that material under any license other than the GNU GPL. Material
    +that is not accepted remains under the GNU GPL alone.
    +
    +Once an Upstream Project has published material under its own license, that
    +license governs the copy the Upstream Project published. Nothing in this
    +document restricts what anyone may do with that copy.
    +
    +This permission does not authorise distribution under any other license for any
    +other purpose. In particular, it does not authorise distributing this material
    +under a permissive license to the public at large, to a fork, or to a
    +redistributor, whether or not an Upstream Project was also asked to take it.
    +
    +
    +2. Upstream Projects
    +--------------------
    +
    +"Upstream Project" means one of the following, and no others:
    +
    +
    +
    +An Upstream Project's "official repository" is the repository named above, or a
    +repository that project's own maintainers designate as its successor. A fork,
    +a mirror, a vendored copy, and a redistribution are not official repositories.
    +
    +
    +3. Notes
    +--------
    +
    +Under GPL version 3 section 7, a recipient may remove these additional
    +permissions from a copy they convey. Doing so does not affect the terms on
    +which the permissions are offered here, and does not withdraw them from anyone
    +who received them.
    +
    +Adding permissions on top of the GNU GPL does not restrict any freedom the GPL
    +grants. Every recipient keeps the full GPL grant regardless of whether they use
    +anything in this document.
    +
    +Contributions to this project are made under this project's license, which
    +includes these additional permissions. A contributor therefore does not need to
    +sign a separate agreement for their contribution to be eligible for upstream
    +submission under section 1. See CONTRIBUTING.md.
    diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
    new file mode 100644
    index 000000000..903a743a9
    --- /dev/null
    +++ b/CONTRIBUTING.md
    @@ -0,0 +1,32 @@
    +# Contributing
    +
    +## Sign-off
    +
    +Every commit must carry a `Signed-off-by:` line certifying the
    +[Developer Certificate of Origin 1.1](https://developercertificate.org/):
    +
    +    git commit -s
    +
    +## What signing off means here
    +
    +The DCO asks you to certify that you have the right to submit your work under
    +the license this project uses. For this project that license is the GNU GPL
    +**plus the additional permissions in `ADDITIONAL-PERMISSIONS`**, so a
    +contribution made under it carries those permissions too.
    +
    +That is deliberate, and it is why this project asks for a DCO rather than a
    +CLA. The additional permissions let anyone relicense material from this project
    +in order to get it accepted into the upstream project it belongs in. If
    +contributions did not carry those permissions, every contribution would become
    +a piece of the tree that could never be sent upstream, and the permission would
    +quietly stop meaning anything as the project grew.
    +
    +You keep your copyright. Nothing is assigned to anyone.
    +
    +## Do not sign off on someone else's license
    +
    +If you are contributing material you did not write -- code ported from another
    +project, a header copied from a kernel, a function translated from elsewhere --
    +say so in the file and keep that material under its own license. Do not put a
    +project SPDX tag on it. Several files in these repositories are in exactly that
    +position and are marked accordingly.
    diff --git a/LICENSING.md b/LICENSING.md
    new file mode 100644
    index 000000000..bedb16593
    --- /dev/null
    +++ b/LICENSING.md
    @@ -0,0 +1,31 @@
    +# Licensing
    +
    +This repository holds two kinds of material and they are licensed differently.
    +
    +## Material inherited from upstream
    +
    +
    +
    +Every file that came from an upstream project stays under that project's
    +license. Nothing here relicenses it, and modifications to those files do not
    +relicense them either — a patched upstream file is still an upstream file.
    +
    +## Material written for DroidVM
    +
    +Files carrying `SPDX-License-Identifier: GPL-3.0-or-later` are DroidVM work
    +and are licensed under the GNU GPL, version 3 or later, **with the
    +additional permissions in `ADDITIONAL-PERMISSIONS`**.
    +
    +Those permissions exist so this work can go upstream. They let anyone
    +relicense it under the terms an upstream project requires, for the purpose of
    +getting it merged there — and only for that purpose. Once upstream publishes
    +it, upstream's license governs that copy.
    +
    +## Third-party material that is neither
    +
    +None. The app is DroidVM work throughout; `app/src/main/java/cn/classfun/droidvm`
    +is the only source tree and nothing is vendored into it.
    +
    +## Contributing
    +
    +See `CONTRIBUTING.md`. Sign-off is required; there is no CLA.
    diff --git a/app/src/main/java/cn/classfun/droidvm/DroidVMApp.java b/app/src/main/java/cn/classfun/droidvm/DroidVMApp.java
    index 4d6a266eb..eaa088420 100644
    --- a/app/src/main/java/cn/classfun/droidvm/DroidVMApp.java
    +++ b/app/src/main/java/cn/classfun/droidvm/DroidVMApp.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm;
     
     import android.app.Application;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/Daemon.java b/app/src/main/java/cn/classfun/droidvm/daemon/Daemon.java
    index 6db9c1229..8389dfb5e 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/Daemon.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/Daemon.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon;
     
     import static android.os.Process.myPid;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/console/ConsoleStream.java b/app/src/main/java/cn/classfun/droidvm/daemon/console/ConsoleStream.java
    index 023fbe257..ca69dbb76 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/console/ConsoleStream.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/console/ConsoleStream.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.console;
     
     import static java.nio.charset.StandardCharsets.UTF_8;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/console/FDPipeConsoleStream.java b/app/src/main/java/cn/classfun/droidvm/daemon/console/FDPipeConsoleStream.java
    index 3525a905e..d1ed98317 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/console/FDPipeConsoleStream.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/console/FDPipeConsoleStream.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.console;
     
     import android.os.ParcelFileDescriptor;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/console/FDSocketConsoleStream.java b/app/src/main/java/cn/classfun/droidvm/daemon/console/FDSocketConsoleStream.java
    index 7e4fe5be8..0dd9d22c5 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/console/FDSocketConsoleStream.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/console/FDSocketConsoleStream.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.console;
     
     import androidx.annotation.NonNull;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/console/InputConsoleStream.java b/app/src/main/java/cn/classfun/droidvm/daemon/console/InputConsoleStream.java
    index bb04720e6..15d3fd4ab 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/console/InputConsoleStream.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/console/InputConsoleStream.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.console;
     
     import androidx.annotation.NonNull;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/console/LocalSocketConsoleStream.java b/app/src/main/java/cn/classfun/droidvm/daemon/console/LocalSocketConsoleStream.java
    index 5aa42749b..dc51f3817 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/console/LocalSocketConsoleStream.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/console/LocalSocketConsoleStream.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.console;
     
     import android.net.LocalSocket;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/console/SimpleConsoleStream.java b/app/src/main/java/cn/classfun/droidvm/daemon/console/SimpleConsoleStream.java
    index 1f268d986..6b63881cc 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/console/SimpleConsoleStream.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/console/SimpleConsoleStream.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.console;
     
     import androidx.annotation.NonNull;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/display/DaemonSystemContext.java b/app/src/main/java/cn/classfun/droidvm/daemon/display/DaemonSystemContext.java
    index e5e4c1f07..c6bc56678 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/display/DaemonSystemContext.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/display/DaemonSystemContext.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.display;
     
     import static cn.classfun.droidvm.lib.utils.StringUtils.fmt;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/display/NativeDisplayBinder.java b/app/src/main/java/cn/classfun/droidvm/daemon/display/NativeDisplayBinder.java
    index 6b866490b..65056c6c1 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/display/NativeDisplayBinder.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/display/NativeDisplayBinder.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.display;
     
     import static cn.classfun.droidvm.lib.utils.StringUtils.fmt;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/basic/AuthHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/basic/AuthHandler.java
    index c918cbf48..400d8bb94 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/basic/AuthHandler.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/basic/AuthHandler.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.ipc.basic;
     
     import static cn.classfun.droidvm.lib.daemon.DaemonHelper.getTokenFile;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/basic/PingHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/basic/PingHandler.java
    index 989d4377e..3e9761807 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/basic/PingHandler.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/basic/PingHandler.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.ipc.basic;
     
     import androidx.annotation.NonNull;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/basic/SetAppConfig.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/basic/SetAppConfig.java
    index 33db76b3b..20e5a375f 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/basic/SetAppConfig.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/basic/SetAppConfig.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.ipc.basic;
     
     import androidx.annotation.NonNull;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/basic/VersionHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/basic/VersionHandler.java
    index 007335536..b5d64b903 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/basic/VersionHandler.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/basic/VersionHandler.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.ipc.basic;
     
     import static cn.classfun.droidvm.daemon.Daemon.daemonHash;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/AddAddressHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/AddAddressHandler.java
    index 2fd9bb10e..231612495 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/AddAddressHandler.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/AddAddressHandler.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.ipc.network;
     
     import static cn.classfun.droidvm.lib.utils.StringUtils.fmt;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/AddInterfaceHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/AddInterfaceHandler.java
    index e2df64f31..640a10979 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/AddInterfaceHandler.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/AddInterfaceHandler.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.ipc.network;
     
     import static cn.classfun.droidvm.lib.utils.StringUtils.fmt;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/CreateHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/CreateHandler.java
    index e1ac41fca..924f77319 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/CreateHandler.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/CreateHandler.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.ipc.network;
     
     import androidx.annotation.NonNull;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/DeleteHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/DeleteHandler.java
    index 37b3e465b..adb641a53 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/DeleteHandler.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/DeleteHandler.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.ipc.network;
     
     import static java.util.UUID.fromString;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/ExistsHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/ExistsHandler.java
    index 5278a1113..52e54d063 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/ExistsHandler.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/ExistsHandler.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.ipc.network;
     
     import androidx.annotation.NonNull;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/InfoHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/InfoHandler.java
    index 33e194943..2db683ed7 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/InfoHandler.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/InfoHandler.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.ipc.network;
     
     import androidx.annotation.NonNull;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/ListHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/ListHandler.java
    index aeefda1a5..262a314d1 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/ListHandler.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/ListHandler.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.ipc.network;
     
     import androidx.annotation.NonNull;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/ListInterfacesHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/ListInterfacesHandler.java
    index 1bda4ec2d..a4f11da1e 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/ListInterfacesHandler.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/ListInterfacesHandler.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.ipc.network;
     
     import androidx.annotation.NonNull;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/ListUplinksHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/ListUplinksHandler.java
    index 98c286258..772d2d258 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/ListUplinksHandler.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/ListUplinksHandler.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.ipc.network;
     
     import androidx.annotation.NonNull;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/ModifyHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/ModifyHandler.java
    index e3569534e..8cbf9fa35 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/ModifyHandler.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/ModifyHandler.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.ipc.network;
     
     import androidx.annotation.NonNull;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/PdReleaseHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/PdReleaseHandler.java
    index 93bc9245d..a13c8f498 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/PdReleaseHandler.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/PdReleaseHandler.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.ipc.network;
     
     import static cn.classfun.droidvm.lib.utils.StringUtils.fmt;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/PdRenewHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/PdRenewHandler.java
    index 1a253a94a..14f775746 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/PdRenewHandler.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/PdRenewHandler.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.ipc.network;
     
     import static cn.classfun.droidvm.lib.utils.StringUtils.fmt;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/RemoveAddressHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/RemoveAddressHandler.java
    index e4b9c6234..62a4bc21f 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/RemoveAddressHandler.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/RemoveAddressHandler.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.ipc.network;
     
     import static cn.classfun.droidvm.lib.utils.StringUtils.fmt;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/RemoveInterfaceHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/RemoveInterfaceHandler.java
    index be458a26e..c98e0f02f 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/RemoveInterfaceHandler.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/RemoveInterfaceHandler.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.ipc.network;
     
     import static cn.classfun.droidvm.lib.utils.StringUtils.fmt;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/StartHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/StartHandler.java
    index bec5959d9..e0e920a6f 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/StartHandler.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/StartHandler.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.ipc.network;
     
     import static cn.classfun.droidvm.lib.utils.StringUtils.fmt;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/StatusHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/StatusHandler.java
    index 712e10ee9..ff0cb6cd4 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/StatusHandler.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/StatusHandler.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.ipc.network;
     
     import static cn.classfun.droidvm.lib.utils.StringUtils.fmt;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/StopHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/StopHandler.java
    index fe81b8008..8fef1942f 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/StopHandler.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/StopHandler.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.ipc.network;
     
     import static cn.classfun.droidvm.lib.utils.StringUtils.fmt;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/ToolLogHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/ToolLogHandler.java
    index bbd4399e7..86dd97779 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/ToolLogHandler.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/ToolLogHandler.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.ipc.network;
     
     import static cn.classfun.droidvm.lib.utils.StringUtils.fmt;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/BootScanHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/BootScanHandler.java
    index 1d09eb7dd..07fd26cc1 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/BootScanHandler.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/BootScanHandler.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.ipc.vm;
     
     import androidx.annotation.NonNull;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ConsoleClearHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ConsoleClearHandler.java
    index e75e2d680..a966ff706 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ConsoleClearHandler.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ConsoleClearHandler.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.ipc.vm;
     
     import androidx.annotation.NonNull;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ConsoleHistoryHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ConsoleHistoryHandler.java
    index 2a8460c14..70c8bbb0d 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ConsoleHistoryHandler.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ConsoleHistoryHandler.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.ipc.vm;
     
     import androidx.annotation.NonNull;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ConsoleInfoHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ConsoleInfoHandler.java
    index d3ab3bae9..fb571fa6d 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ConsoleInfoHandler.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ConsoleInfoHandler.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.ipc.vm;
     
     import androidx.annotation.NonNull;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ConsoleListHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ConsoleListHandler.java
    index 7eceecba2..ccbbeb51c 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ConsoleListHandler.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ConsoleListHandler.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.ipc.vm;
     
     import androidx.annotation.NonNull;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ConsoleWriteHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ConsoleWriteHandler.java
    index c3694081b..2ce19763e 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ConsoleWriteHandler.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ConsoleWriteHandler.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.ipc.vm;
     
     import androidx.annotation.NonNull;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ControlHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ControlHandler.java
    index d5616a46c..82a8d62e2 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ControlHandler.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ControlHandler.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.ipc.vm;
     
     import static cn.classfun.droidvm.lib.utils.StringUtils.fmt;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/CreateHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/CreateHandler.java
    index 6373cd38f..3a97efed3 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/CreateHandler.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/CreateHandler.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.ipc.vm;
     
     import androidx.annotation.NonNull;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/DeleteHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/DeleteHandler.java
    index 4ca0bd72d..4e30e5c8f 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/DeleteHandler.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/DeleteHandler.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.ipc.vm;
     
     import static cn.classfun.droidvm.lib.utils.StringUtils.fmt;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/DisplayAttachHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/DisplayAttachHandler.java
    index 043f1dd5f..e20c94e55 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/DisplayAttachHandler.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/DisplayAttachHandler.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.ipc.vm;
     
     import androidx.annotation.NonNull;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ExistsHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ExistsHandler.java
    index 2bd6c5703..63fdea58a 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ExistsHandler.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ExistsHandler.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.ipc.vm;
     
     import androidx.annotation.NonNull;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ExportHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ExportHandler.java
    index dab0c1323..a915e0e8f 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ExportHandler.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ExportHandler.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.ipc.vm;
     
     import androidx.annotation.NonNull;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/GetHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/GetHandler.java
    index 44be4febf..f7ae0c49c 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/GetHandler.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/GetHandler.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.ipc.vm;
     
     import static cn.classfun.droidvm.lib.utils.StringUtils.fmt;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ImportHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ImportHandler.java
    index caac7fe4f..388293442 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ImportHandler.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ImportHandler.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.ipc.vm;
     
     import androidx.annotation.NonNull;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/InputHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/InputHandler.java
    index 15f771e40..0bcc6c209 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/InputHandler.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/InputHandler.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.ipc.vm;
     
     import static cn.classfun.droidvm.lib.utils.StringUtils.fmt;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ListHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ListHandler.java
    index 83eaf1e4f..ef049838e 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ListHandler.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ListHandler.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.ipc.vm;
     
     import androidx.annotation.NonNull;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ModifyHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ModifyHandler.java
    index 8d7b92200..dd8e9ab67 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ModifyHandler.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ModifyHandler.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.ipc.vm;
     
     import androidx.annotation.NonNull;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/RebootHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/RebootHandler.java
    index fb3af10a8..1d131dd73 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/RebootHandler.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/RebootHandler.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.ipc.vm;
     
     import static cn.classfun.droidvm.lib.utils.StringUtils.fmt;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ResumeHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ResumeHandler.java
    index dfd9991dd..aca06f6d1 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ResumeHandler.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/ResumeHandler.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.ipc.vm;
     
     import static cn.classfun.droidvm.lib.utils.StringUtils.fmt;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/StartHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/StartHandler.java
    index 99922c352..b7bb2ccb5 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/StartHandler.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/StartHandler.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.ipc.vm;
     
     import static cn.classfun.droidvm.lib.utils.StringUtils.fmt;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/StatusHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/StatusHandler.java
    index 650af3562..7aaaf6867 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/StatusHandler.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/StatusHandler.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.ipc.vm;
     
     import static cn.classfun.droidvm.lib.utils.StringUtils.fmt;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/StopAllHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/StopAllHandler.java
    index 305d89168..174f69c34 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/StopAllHandler.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/StopAllHandler.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.ipc.vm;
     
     import androidx.annotation.NonNull;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/StopHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/StopHandler.java
    index 06af66173..1a072c5b6 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/StopHandler.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/StopHandler.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.ipc.vm;
     
     import static cn.classfun.droidvm.lib.utils.StringUtils.fmt;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/SuspendHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/SuspendHandler.java
    index a70a1c98f..24d5b3a06 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/SuspendHandler.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/SuspendHandler.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.ipc.vm;
     
     import static cn.classfun.droidvm.lib.utils.StringUtils.fmt;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/VncInfoHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/VncInfoHandler.java
    index 30c366e28..21329dd48 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/VncInfoHandler.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/VncInfoHandler.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.ipc.vm;
     
     import androidx.annotation.NonNull;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/NetworkInstance.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/NetworkInstance.java
    index 87715f4fd..fccaf77f3 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/network/NetworkInstance.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/NetworkInstance.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.network;
     
     import static cn.classfun.droidvm.lib.utils.StringUtils.fmt;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/NetworkInstanceStore.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/NetworkInstanceStore.java
    index e69a9975d..cecfb4ff8 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/network/NetworkInstanceStore.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/NetworkInstanceStore.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.network;
     
     import static cn.classfun.droidvm.lib.store.network.NetworkState.RUNNING;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/NetworkWatchdog.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/NetworkWatchdog.java
    index 40c08c566..cd7e5ad47 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/network/NetworkWatchdog.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/NetworkWatchdog.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.network;
     
     import android.util.Log;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/BackendBase.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/BackendBase.java
    index 1786d6c42..82c4c1bce 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/BackendBase.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/BackendBase.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.network.backend;
     
     import androidx.annotation.NonNull;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/BridgeBackend.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/BridgeBackend.java
    index 522ec52d7..a9bf3996c 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/BridgeBackend.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/BridgeBackend.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.network.backend;
     
     import androidx.annotation.NonNull;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/BridgeDhcp.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/BridgeDhcp.java
    index b9d5c41a9..f7bf966df 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/BridgeDhcp.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/BridgeDhcp.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.network.backend;
     
     import static cn.classfun.droidvm.lib.Constants.DATA_DIR;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/DefaultRouterWatcher.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/DefaultRouterWatcher.java
    index c6b8e59b2..4f5a86474 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/DefaultRouterWatcher.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/DefaultRouterWatcher.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.network.backend;
     
     import static cn.classfun.droidvm.lib.utils.FileUtils.shellReadFile;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/FirewallHelper.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/FirewallHelper.java
    index 9d383b580..4f973d9e6 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/FirewallHelper.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/FirewallHelper.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.network.backend;
     
     import androidx.annotation.NonNull;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/LinuxBridgeBackend.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/LinuxBridgeBackend.java
    index f33e4e408..c35df5588 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/LinuxBridgeBackend.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/LinuxBridgeBackend.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.network.backend;
     
     import static cn.classfun.droidvm.lib.utils.StringUtils.fmt;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/LinuxNetwork.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/LinuxNetwork.java
    index 0ae109da8..5ace9abd3 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/LinuxNetwork.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/LinuxNetwork.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.network.backend;
     
     import static cn.classfun.droidvm.lib.utils.RunUtils.run;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/ManagedProcess.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/ManagedProcess.java
    index 59b7952cd..379c8ea92 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/ManagedProcess.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/ManagedProcess.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.network.backend;
     
     import static cn.classfun.droidvm.lib.utils.StringUtils.fmt;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/Netbox.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/Netbox.java
    index 3d08b0783..0a0e58ba3 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/Netbox.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/Netbox.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.network.backend;
     
     import static cn.classfun.droidvm.lib.utils.AssetUtils.getAssetBinaryPath;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/Pbridge.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/Pbridge.java
    index 2d91c8b7f..5a2684bd7 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/Pbridge.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/Pbridge.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.network.backend;
     
     import static cn.classfun.droidvm.lib.Constants.*;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/UplinkResolver.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/UplinkResolver.java
    index 402f8ab4c..00744e255 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/UplinkResolver.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/UplinkResolver.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.network.backend;
     
     import static cn.classfun.droidvm.lib.utils.StringUtils.fmt;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/gvisor/GvisorBridgeBackend.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/gvisor/GvisorBridgeBackend.java
    index e6c8bb6cf..95e42cc53 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/gvisor/GvisorBridgeBackend.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/gvisor/GvisorBridgeBackend.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.network.backend.gvisor;
     
     import static cn.classfun.droidvm.lib.Constants.DATA_DIR;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/gvisor/GvswitchClient.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/gvisor/GvswitchClient.java
    index 23b598f08..379107f35 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/gvisor/GvswitchClient.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/gvisor/GvswitchClient.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.network.backend.gvisor;
     
     import static cn.classfun.droidvm.lib.utils.StringUtils.fmt;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/gvisor/GvswitchConfigBuilder.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/gvisor/GvswitchConfigBuilder.java
    index 01e16c223..e48a55773 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/gvisor/GvswitchConfigBuilder.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/gvisor/GvswitchConfigBuilder.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.network.backend.gvisor;
     
     import androidx.annotation.NonNull;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/iptables/ChainInfo.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/iptables/ChainInfo.java
    index 053e81876..37db8748f 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/iptables/ChainInfo.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/iptables/ChainInfo.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.network.backend.iptables;
     
     import androidx.annotation.NonNull;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/iptables/IptablesBackend.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/iptables/IptablesBackend.java
    index f98e19dec..0df24ede0 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/iptables/IptablesBackend.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/iptables/IptablesBackend.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.network.backend.iptables;
     
     import static cn.classfun.droidvm.lib.utils.StringUtils.fmt;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/iptables/IptablesNetworkInstance.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/iptables/IptablesNetworkInstance.java
    index a8224c476..b8e8424dc 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/iptables/IptablesNetworkInstance.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/iptables/IptablesNetworkInstance.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.network.backend.iptables;
     
     import static cn.classfun.droidvm.lib.utils.StringUtils.fmt;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/pd/Duid.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/pd/Duid.java
    index 844e0ba29..3ee600f2a 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/pd/Duid.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/pd/Duid.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.network.backend.pd;
     
     import static cn.classfun.droidvm.lib.utils.StringUtils.fmt;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/server/ClientHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/server/ClientHandler.java
    index c5474f241..bd0a01b82 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/server/ClientHandler.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/server/ClientHandler.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.server;
     
     import static cn.classfun.droidvm.lib.utils.StringUtils.fmt;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/server/ClientRequest.java b/app/src/main/java/cn/classfun/droidvm/daemon/server/ClientRequest.java
    index c5286302b..273b21dab 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/server/ClientRequest.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/server/ClientRequest.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.server;
     
     import androidx.annotation.NonNull;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/server/ClientResponse.java b/app/src/main/java/cn/classfun/droidvm/daemon/server/ClientResponse.java
    index b57ba0ddc..4fafb2262 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/server/ClientResponse.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/server/ClientResponse.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.server;
     
     import androidx.annotation.NonNull;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/server/RequestException.java b/app/src/main/java/cn/classfun/droidvm/daemon/server/RequestException.java
    index bb5bbde8d..39a68e4f9 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/server/RequestException.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/server/RequestException.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.server;
     
     public final class RequestException extends RuntimeException {
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/server/RequestHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/server/RequestHandler.java
    index 5e0a1b6b2..373938c16 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/server/RequestHandler.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/server/RequestHandler.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.server;
     
     import androidx.annotation.NonNull;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/server/RequestHandlerStore.java b/app/src/main/java/cn/classfun/droidvm/daemon/server/RequestHandlerStore.java
    index 0aa60ebc9..516cc8dfa 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/server/RequestHandlerStore.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/server/RequestHandlerStore.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.server;
     
     import static cn.classfun.droidvm.lib.utils.StringUtils.fmt;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/server/Server.java b/app/src/main/java/cn/classfun/droidvm/daemon/server/Server.java
    index 061b554e6..294084fce 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/server/Server.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/server/Server.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.server;
     
     import static android.system.Os.stat;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/server/ServerContext.java b/app/src/main/java/cn/classfun/droidvm/daemon/server/ServerContext.java
    index 30607aab8..82c8e9c54 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/server/ServerContext.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/server/ServerContext.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.server;
     
     import static cn.classfun.droidvm.lib.Constants.DATA_DIR;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/BootPlan.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/BootPlan.java
    index 39a097b8b..2a14a3806 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/BootPlan.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/BootPlan.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.vm;
     
     import static java.util.concurrent.TimeUnit.MILLISECONDS;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/SerialPipe.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/SerialPipe.java
    index 935fc3193..f0ffe4559 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/SerialPipe.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/SerialPipe.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.vm;
     
     import java.io.Closeable;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMBackendInstance.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMBackendInstance.java
    index 28f4b4ea9..79a8d3caa 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMBackendInstance.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMBackendInstance.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.vm;
     
     import static cn.classfun.droidvm.lib.Constants.DATA_DIR;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMInstance.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMInstance.java
    index 9da40e025..e54d1790f 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMInstance.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMInstance.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.vm;
     
     import static java.util.Objects.requireNonNull;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMInstanceStore.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMInstanceStore.java
    index b8523620e..00f05dd59 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMInstanceStore.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMInstanceStore.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.vm;
     
     import static cn.classfun.droidvm.daemon.vm.VMInstance.getVMInstance;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMStartResult.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMStartResult.java
    index 4d292d0f9..8284786ff 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMStartResult.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMStartResult.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.vm;
     
     import cn.classfun.droidvm.lib.natives.NativeProcess;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/BackendBase.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/BackendBase.java
    index 41de65647..85c3b4ba5 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/BackendBase.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/BackendBase.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.vm.backend;
     
     import static cn.classfun.droidvm.lib.utils.StringUtils.fmt;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackend.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackend.java
    index 37185f3c0..b42f15202 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackend.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackend.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.vm.backend;
     
     import androidx.annotation.NonNull;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackendInstance.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackendInstance.java
    index af3050084..6f62a1819 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackendInstance.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackendInstance.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.vm.backend;
     
     import static android.net.LocalSocketAddress.Namespace.FILESYSTEM;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/NativeDisplayInputBridge.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/NativeDisplayInputBridge.java
    index af5a3c9ba..a6707065c 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/NativeDisplayInputBridge.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/NativeDisplayInputBridge.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.vm.backend;
     
     import static cn.classfun.droidvm.lib.utils.FileUtils.deleteFile;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/QemuBackend.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/QemuBackend.java
    index 6479786d8..4bb9fde00 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/QemuBackend.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/QemuBackend.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.vm.backend;
     
     import androidx.annotation.NonNull;
    diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/QemuBackendInstance.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/QemuBackendInstance.java
    index c8e33c794..0fef5f5ea 100644
    --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/QemuBackendInstance.java
    +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/QemuBackendInstance.java
    @@ -1,3 +1,6 @@
    +// SPDX-License-Identifier: GPL-3.0-or-later
    +// Copyright DroidVM contributors
    +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root.
     package cn.classfun.droidvm.daemon.vm.backend;
     
     import static android.net.LocalSocketAddress.Namespace.FILESYSTEM;
    @@ -37,13 +40,16 @@
     import cn.classfun.droidvm.lib.natives.NativeProcess;
     import cn.classfun.droidvm.lib.store.base.DataItem;
     import cn.classfun.droidvm.lib.store.disk.DiskBus;
    -import cn.classfun.droidvm.lib.store.vm.DisplayBackend;
    +import cn.classfun.droidvm.lib.store.vm.DisplayExporter;
    +import cn.classfun.droidvm.lib.store.vm.VMScreenConfig;
     import cn.classfun.droidvm.lib.store.vm.GpuBackend;
    +import cn.classfun.droidvm.lib.store.vm.PeripheralType;
     import cn.classfun.droidvm.lib.store.vm.ProtectedVM;
     import cn.classfun.droidvm.lib.store.vm.SharedDirCache;
     import cn.classfun.droidvm.lib.store.vm.VMBackend;
     import cn.classfun.droidvm.lib.store.vm.VMConfig;
     import cn.classfun.droidvm.lib.store.vm.VMHypervisor;
    +import cn.classfun.droidvm.lib.store.vm.VMPeripheralConfig;
     
     @SuppressWarnings("FieldCanBeLocal")
     public final class QemuBackendInstance extends VMBackendInstance {
    @@ -51,9 +57,13 @@ public final class QemuBackendInstance extends VMBackendInstance {
         private static final String RUN_PATH = pathJoin(DATA_DIR, "run");
         private String qmpSocketPath = null;
         private String uartSocketPath = null;
    +    private String agentSocketPath = null;
         private int ioThreadCounter = 0;
         private int driveCounter = 0;
    +    private final boolean agentMode;
         private final LocalSocketConsoleStream uartStream;
    +    @Nullable
    +    private final LocalSocketConsoleStream agentStream;
         private final InputConsoleStream stdoutStream;
         private final InputConsoleStream stderrStream;
         private final SimpleConsoleStream stdioStream;
    @@ -63,7 +73,12 @@ public QemuBackendInstance(
             @NonNull VMConfig config
         ) {
             super(context, config);
    +        agentMode = config.item.optBoolean("agent_mode", false);
             uartStream = new LocalSocketConsoleStream(config, "uart", null);
    +        agentStream = agentMode
    +            ? new LocalSocketConsoleStream(config, "agent", null) : null;
    +        if (agentStream != null)
    +            agentStream.setPersistentLogEnabled(false);
             stdoutStream = new InputConsoleStream(config, "stdout", null);
             stderrStream = new InputConsoleStream(config, "stderr", null);
             stdioStream = new SimpleConsoleStream(config, "stdio");
    @@ -71,6 +86,7 @@ public QemuBackendInstance(
             addStream(stderrStream);
             addStream(stdioStream);
             addStream(uartStream);
    +        if (agentStream != null) addStream(agentStream);
         }
     
         @NonNull
    @@ -83,6 +99,10 @@ public VMStartResult start() {
             deleteFile(qmpSocketPath);
             uartSocketPath = pathJoin(RUN_PATH, fmt("%s-uart.sock", config.getName()));
             deleteFile(uartSocketPath);
    +        if (agentMode) {
    +            agentSocketPath = pathJoin(RUN_PATH, fmt("%s-agent.sock", config.getName()));
    +            deleteFile(agentSocketPath);
    +        }
             Log.i(TAG, fmt("QMP socket path: %s", qmpSocketPath));
             var args = buildCommand();
             Log.i(TAG, fmt("Executing: %s", String.join(" ", args)));
    @@ -97,23 +117,32 @@ public VMStartResult start() {
                 Log.e(TAG, "Failed to start qemu process", e);
                 return result;
             }
    -        waitForUartClient();
    +        // agent0 is declared before the blocking UART chardev. Connect it first, then release
    +        // QEMU's UART wait so no hvc0 boot output can race ahead of the daemon reader.
    +        if (agentStream != null)
    +            waitForSocketClient("agent", agentSocketPath, agentStream);
    +        waitForSocketClient("UART", uartSocketPath, uartStream);
             return result;
         }
     
    -    private void waitForUartClient() {
    +    private void waitForSocketClient(
    +        @NonNull String label,
    +        @NonNull String socketPath,
    +        @NonNull LocalSocketConsoleStream stream
    +    ) {
             int i = 0;
    -        Log.i(TAG, fmt("UART socket path: %s", uartSocketPath));
    +        Log.i(TAG, fmt("%s socket path: %s", label, socketPath));
             while (true) {
                 try {
    -                var uart = new LocalSocket(LocalSocket.SOCKET_STREAM);
    -                uart.connect(new LocalSocketAddress(uartSocketPath, FILESYSTEM));
    -                Log.i(TAG, "UART client connected");
    -                uartStream.setSocket(uart);
    +                var socket = new LocalSocket(LocalSocket.SOCKET_STREAM);
    +                socket.connect(new LocalSocketAddress(socketPath, FILESYSTEM));
    +                Log.i(TAG, fmt("%s client connected", label));
    +                stream.setSocket(socket);
                     return;
                 } catch (Exception e) {
                     if (i >= 50) {
    -                    Log.e(TAG, "failed to create UART socket after multiple attempts, giving up");
    +                    Log.e(TAG, fmt(
    +                        "failed to connect %s socket after multiple attempts, giving up", label));
                         throw new RuntimeException(e);
                     }
                     i++;
    @@ -134,8 +163,7 @@ private List buildCommand() {
             args.add(pathJoin(DATA_DIR, "usr", "share", "qemu"));
             var hyp = item.optString("hypervisor", "auto");
             var hypervisor = VMHypervisor.valueOf(hyp.toUpperCase());
    -        if (hypervisor == VMHypervisor.AUTO)
    -            hypervisor = VMHypervisor.findPreferredHypervisor(VMBackend.QEMU);
    +        hypervisor = VMHypervisor.resolveConfigured(VMBackend.QEMU, hypervisor);
             if (hypervisor == null) throw new RuntimeException("No supported hypervisor found for QEMU backend");
             args.add("-accel");
             var defProtectedMode = ProtectedVM.PROTECTED_NORMAL;
    @@ -182,7 +210,7 @@ private List buildCommand() {
             switch (protectedVm) {
                 case PROTECTED_PROTECTED:
                 case PROTECTED_WITHOUT_FIRMWARE:
    -                long swiotlbMb = Math.max(item.optLong("swiotlb_mb", 64), 1);
    +                long swiotlbMb = Math.max(item.optLong("swiotlb_mb", 256), 1);
                     args.add("-object");
                     args.add(fmt("arm-confidential-guest,id=prot0,swiotlb-size=%dM", swiotlbMb));
                     break;
    @@ -211,7 +239,7 @@ private List buildCommand() {
             }
             if (item.optBoolean("hugepages", true))
                 args.add("-mem-prealloc");
    -        if (item.optBoolean("rng", true)) {
    +        if (!agentMode && item.optBoolean("rng", true)) {
                 args.add("-object");
                 args.add("rng-random,filename=/dev/urandom,id=rng0");
                 args.add("-device");
    @@ -221,14 +249,25 @@ private List buildCommand() {
                 args.add("-device");
                 args.add("virtio-balloon-pci,disable-legacy=on,disable-modern=off");
             }
    -        buildInputCommand(args);
    -        buildUsbCommand(args);
    +        if (!agentMode) buildInputCommand(args);
    +        if (!agentMode) buildUsbCommand(args);
             buildDiskCommand(args);
    -        buildNetCommand(args);
    -        buildSharedDirCommand(args);
    -        buildAudioCommand(args);
    -        buildGpuCommand(args);
    -        buildVncCommand(args);
    +        if (!agentMode) {
    +            buildNetCommand(args);
    +            buildSharedDirCommand(args);
    +            buildAudioCommand(args);
    +            buildGpuCommand(args);
    +            buildVncCommand(args);
    +        }
    +        if (agentMode) {
    +            args.add("-chardev");
    +            args.add(fmt(
    +                "socket,id=agent0,path=%s,server=on,wait=off", agentSocketPath));
    +            args.add("-device");
    +            args.add("virtio-serial-pci,id=agent-bus,disable-legacy=on,disable-modern=off");
    +            args.add("-device");
    +            args.add("virtconsole,chardev=agent0,name=org.droidvm.agent");
    +        }
             args.add("-chardev");
             args.add(fmt("socket,id=uart0,path=%s,server=on,wait=on", uartSocketPath));
             args.add("-serial");
    @@ -308,7 +347,9 @@ private void buildDiskCommand(@NonNull List args) {
                         args.add(fmt("iothread,id=%s", ioId));
                         var driveArg = new StringBuilder();
                         driveArg.append(fmt("file=%s,if=none,id=%s", path, drId));
    -                    driveArg.append(",cache=unsafe,aio=threads,discard=unmap");
    +                    driveArg.append(agentMode
    +                        ? ",cache=writeback,aio=threads,discard=unmap"
    +                        : ",cache=unsafe,aio=threads,discard=unmap");
                         if (readonly) driveArg.append(",readonly=on");
                         args.add("-drive");
                         args.add(driveArg.toString());
    @@ -323,7 +364,7 @@ private void buildDiskCommand(@NonNull List args) {
         }
     
         private void buildInputCommand(@NonNull List args) {
    -        if (!config.item.optBoolean("display_enabled", false)) return;
    +        if (!hasAnyScreen()) return;
             args.add("-device");
             args.add("virtio-multitouch-pci,disable-legacy=on,disable-modern=off");
             args.add("-device");
    @@ -334,7 +375,7 @@ private void buildUsbCommand(@NonNull List args) {
             if (!config.item.optBoolean("usb", true)) return;
             args.add("-device");
             args.add("qemu-xhci,id=usb-bus,p2=15,p3=15");
    -        if (config.item.optBoolean("display_enabled", false)) {
    +        if (hasAnyScreen()) {
                 args.add("-device");
                 args.add("usb-tablet,bus=usb-bus.0");
                 args.add("-device");
    @@ -368,9 +409,27 @@ private void buildSharedDirCommand(@NonNull List args) {
             }
         }
     
    +    /**
    +     * One virtio-sound-pci card on QEMU's aaudio driver, present when the VM has any audio
    +     * peripheral.
    +     *
    +     * 

    Only the on/off decision is plumbed here. The mode, host endpoint, buffer and underrun + * settings of a VirtIO Sound peripheral are all crosvm-side concepts -- QEMU's aaudio driver + * opens whatever the platform routes to, in one process, as whatever uid QEMU runs as -- so + * they are ignored rather than half-honoured. Intel HDA is ignored here too, even though + * this QEMU does emulate one: wiring it would be a separate piece of work with its own + * verification. A config that predates the peripheral tab still works through the old + * {@code audio_enabled} key, which also stays available as an override.

    + */ private void buildAudioCommand(@NonNull List args) { - var displayEnabled = config.item.optBoolean("display_enabled", false); - if (!config.item.optBoolean("audio_enabled", displayEnabled)) return; + boolean anyAudio = false; + for (var peripheral : VMPeripheralConfig.listOf(config.item)) { + if (peripheral.getType() == PeripheralType.VIRTIO_SOUND) + anyAudio = true; + } + if (!anyAudio) { + if (!config.item.optBoolean("audio_enabled", hasAnyScreen())) return; + } args.add("-audiodev"); args.add("aaudio,id=snd0"); args.add("-device"); @@ -411,13 +470,20 @@ private void buildNetCommand(@NonNull List args) { } } + /** + * The virtio-gpu device (with its renderer, if any) and QEMU's nearest thing to simplefb. + * + *

    The virtio-gpu screen's switch is the device, exactly as on crosvm; the renderer only + * decides which QEMU device model implements it. The geometry is that screen's own, so a VM + * with both screens no longer has to give them one size.

    + */ private void buildGpuCommand(@NonNull List args) { var item = config.item; - var useGpu = item.optBoolean("gpu_enabled", false); - var useDisplay = item.optBoolean("display_enabled", false); - if (!useGpu && !useDisplay) return; - var backend = optEnum(item, "display_backend", DisplayBackend.NONE); - if (useGpu) { + var gpuScreen = isScreenEnabled(VMScreenConfig.ID_GPU0); + var fbScreen = isScreenEnabled(VMScreenConfig.ID_SIMPLEFB); + if (!gpuScreen && !fbScreen) return; + if (gpuScreen) { + var gpu0 = VMScreenConfig.of(item, VMScreenConfig.ID_GPU0); var gpuBackend = optEnum(item, "gpu_backend", GpuBackend.NONE); var gpuArg = new StringBuilder(); boolean use3d = false; @@ -431,16 +497,14 @@ private void buildGpuCommand(@NonNull List args) { use3d = true; break; default: + // 2D, or a config that never named a renderer: the plain device, which is what + // "display without acceleration" is on this backend too. gpuArg.append("virtio-gpu-pci"); break; } gpuArg.append(",disable-legacy=on,disable-modern=off"); - if (useDisplay && backend == DisplayBackend.VIRTIO_GPU) { - long width = item.optLong("display_width", 1280); - long height = item.optLong("display_height", 720); - gpuArg.append(fmt(",xres=%d,yres=%d", width, height)); - gpuArg.append(",edid=on"); - } + gpuArg.append(fmt(",xres=%d,yres=%d", gpu0.getWidth(), gpu0.getHeight())); + gpuArg.append(",edid=on"); if (use3d) { gpuArg.append(",blob=on"); args.add("-display"); @@ -448,30 +512,51 @@ private void buildGpuCommand(@NonNull List args) { } args.add("-device"); args.add(gpuArg.toString()); - } else if (backend == DisplayBackend.VIRTIO_GPU) { - long width = item.optLong("display_width", 1280); - long height = item.optLong("display_height", 720); - args.add("-device"); - args.add(fmt("virtio-gpu-pci,disable-legacy=on,disable-modern=off,xres=%d,yres=%d,edid=on", - width, height)); } - if (useDisplay && backend == DisplayBackend.SIMPLEFB) { + // ramfb takes no size: QEMU's firmware-programmed framebuffer gets its geometry from the + // guest, so the simplefb screen's width and height have nowhere to go here. + if (fbScreen) { args.add("-device"); args.add("ramfb"); } } + /** + * Whether the config asks for [screenId]'s display device. + * + *

    QEMU has no {@code screen=} to bind an exporter to -- one {@code -vnc} serves whatever + * the machine displays -- so the per-screen model only reaches this far: the screen switches + * decide which devices exist, and the first screen exporting over VNC supplies the server's + * settings. Native display is refused for this backend in the editor, so it never appears + * here at all.

    + */ + private boolean hasAnyScreen() { + for (var screen : VMScreenConfig.listOf(config.item)) + if (screen.isEnabled()) return true; + return false; + } + + private boolean isScreenEnabled(@NonNull String screenId) { + var screen = VMScreenConfig.find(config.item, screenId); + return screen != null && screen.isEnabled(); + } + private void buildVncCommand(@NonNull List args) { - var item = config.item; - if (!item.optBoolean("vnc_enabled", false)) return; - var host = item.optString("vnc_host", "0.0.0.0"); + VMScreenConfig bound = null; + for (var screen : VMScreenConfig.listOf(config.item)) + if (screen.isEnabled() && screen.getExporter() == DisplayExporter.VNC) { + bound = screen; + break; + } + if (bound == null) return; + var host = bound.getVncHost(); if (host.isEmpty()) host = "0.0.0.0"; - long port = Math.max(item.optLong("vnc_port", 5900), 1); + long port = Math.max(bound.getVncPort(), 1); long displayNum = port - 5900; if (displayNum < 0) displayNum = 0; var vncArg = new StringBuilder(); vncArg.append(fmt("%s:%d", host, displayNum)); - var password = item.optString("vnc_password", ""); + var password = bound.getVncPassword(); if (!password.isEmpty()) { args.add("-object"); args.add(fmt("secret,id=vnc-password,data=%s", password)); @@ -575,6 +660,7 @@ public boolean hasControlSocket() { @Override public void cleanup() { uartStream.close(); + if (agentStream != null) agentStream.close(); if (qmpSocketPath != null) { deleteFile(qmpSocketPath); qmpSocketPath = null; @@ -583,5 +669,9 @@ public void cleanup() { deleteFile(uartSocketPath); uartSocketPath = null; } + if (agentSocketPath != null) { + deleteFile(agentSocketPath); + agentSocketPath = null; + } } } diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/pkg/VMExportTask.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/pkg/VMExportTask.java index 8a8520c39..7addc1cb7 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/pkg/VMExportTask.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/pkg/VMExportTask.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.daemon.vm.pkg; import static java.nio.charset.StandardCharsets.UTF_8; diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/pkg/VMExportUtils.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/pkg/VMExportUtils.java index 83f382244..1611d9cda 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/pkg/VMExportUtils.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/pkg/VMExportUtils.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.daemon.vm.pkg; import static java.nio.charset.StandardCharsets.UTF_8; diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/pkg/VMImportTask.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/pkg/VMImportTask.java index 9f7926007..fb8b043f3 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/pkg/VMImportTask.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/pkg/VMImportTask.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.daemon.vm.pkg; import static cn.classfun.droidvm.daemon.vm.pkg.VMImportUtils.remapBootPaths; diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/pkg/VMImportUtils.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/pkg/VMImportUtils.java index 4f47f12a3..e175a65ed 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/pkg/VMImportUtils.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/pkg/VMImportUtils.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.daemon.vm.pkg; import static java.util.Objects.requireNonNull; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/Constants.java b/app/src/main/java/cn/classfun/droidvm/lib/Constants.java index 852e863f8..6a082d818 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/Constants.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/Constants.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib; import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/api/ApiInfo.java b/app/src/main/java/cn/classfun/droidvm/lib/api/ApiInfo.java index 47fdde35b..c6725f772 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/api/ApiInfo.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/api/ApiInfo.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.api; import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/api/ApiManager.java b/app/src/main/java/cn/classfun/droidvm/lib/api/ApiManager.java index b1f1cabd9..753b8c6e1 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/api/ApiManager.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/api/ApiManager.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.api; import static java.util.Objects.requireNonNull; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/api/ApiServiceInfo.java b/app/src/main/java/cn/classfun/droidvm/lib/api/ApiServiceInfo.java index cd35cee58..e5f90dc0e 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/api/ApiServiceInfo.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/api/ApiServiceInfo.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.api; import androidx.annotation.NonNull; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/api/Privacy.java b/app/src/main/java/cn/classfun/droidvm/lib/api/Privacy.java index 118fe5181..7de20ecd4 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/api/Privacy.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/api/Privacy.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.api; import static cn.classfun.droidvm.lib.utils.AssetUtils.loadFromAssets; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/archive/Compression.java b/app/src/main/java/cn/classfun/droidvm/lib/archive/Compression.java index 059234a2c..fb0c732e8 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/archive/Compression.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/archive/Compression.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.archive; import android.content.Context; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/archive/LimitedInputStream.java b/app/src/main/java/cn/classfun/droidvm/lib/archive/LimitedInputStream.java index 451ccfa0c..3b3a26485 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/archive/LimitedInputStream.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/archive/LimitedInputStream.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.archive; import androidx.annotation.NonNull; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/archive/OutputStreamLimiter.java b/app/src/main/java/cn/classfun/droidvm/lib/archive/OutputStreamLimiter.java index 8abc2991d..bd6ff1f9a 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/archive/OutputStreamLimiter.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/archive/OutputStreamLimiter.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.archive; import androidx.annotation.NonNull; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/archive/RandomAccessFileOutputStream.java b/app/src/main/java/cn/classfun/droidvm/lib/archive/RandomAccessFileOutputStream.java index 6d32422cf..4a968d59a 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/archive/RandomAccessFileOutputStream.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/archive/RandomAccessFileOutputStream.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.archive; import androidx.annotation.NonNull; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/archive/TarReader.java b/app/src/main/java/cn/classfun/droidvm/lib/archive/TarReader.java index 34f9b55e5..71ca38108 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/archive/TarReader.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/archive/TarReader.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.archive; import static java.nio.charset.StandardCharsets.UTF_8; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/archive/TarWriter.java b/app/src/main/java/cn/classfun/droidvm/lib/archive/TarWriter.java index c47f01209..59ee04be9 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/archive/TarWriter.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/archive/TarWriter.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.archive; import static java.nio.charset.StandardCharsets.US_ASCII; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/crypt/HashFile.java b/app/src/main/java/cn/classfun/droidvm/lib/crypt/HashFile.java index 1e81355e8..4ffd92ec8 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/crypt/HashFile.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/crypt/HashFile.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.crypt; import static cn.classfun.droidvm.lib.utils.FileUtils.loadJSONFile; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/crypt/HashItem.java b/app/src/main/java/cn/classfun/droidvm/lib/crypt/HashItem.java index eba503a79..97b8b0781 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/crypt/HashItem.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/crypt/HashItem.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.crypt; import androidx.annotation.NonNull; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/daemon/DaemonClient.java b/app/src/main/java/cn/classfun/droidvm/lib/daemon/DaemonClient.java index fcd02dfbb..d05767353 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/daemon/DaemonClient.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/daemon/DaemonClient.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.daemon; import static cn.classfun.droidvm.lib.daemon.Protocol.IPC_REQUEST_TIMEOUT_MS; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/daemon/DaemonConnection.java b/app/src/main/java/cn/classfun/droidvm/lib/daemon/DaemonConnection.java index 3fb894f41..3588af707 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/daemon/DaemonConnection.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/daemon/DaemonConnection.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.daemon; import static cn.classfun.droidvm.lib.daemon.DaemonHelper.getPortFile; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/daemon/DaemonHelper.java b/app/src/main/java/cn/classfun/droidvm/lib/daemon/DaemonHelper.java index 3668efab7..50a8d69b6 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/daemon/DaemonHelper.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/daemon/DaemonHelper.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.daemon; import static android.widget.Toast.LENGTH_LONG; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/daemon/ForegroundCallback.java b/app/src/main/java/cn/classfun/droidvm/lib/daemon/ForegroundCallback.java index a210ab140..bc782b65e 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/daemon/ForegroundCallback.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/daemon/ForegroundCallback.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.daemon; import org.json.JSONObject; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/daemon/PendingRequest.java b/app/src/main/java/cn/classfun/droidvm/lib/daemon/PendingRequest.java index 768d3eaf5..afcd7ef7a 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/daemon/PendingRequest.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/daemon/PendingRequest.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.daemon; import org.json.JSONObject; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/daemon/Protocol.java b/app/src/main/java/cn/classfun/droidvm/lib/daemon/Protocol.java index 5668a2c64..46407334c 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/daemon/Protocol.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/daemon/Protocol.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.daemon; import static java.nio.ByteOrder.LITTLE_ENDIAN; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/daemon/RequestContext.java b/app/src/main/java/cn/classfun/droidvm/lib/daemon/RequestContext.java index a8f893202..497f8d9f5 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/daemon/RequestContext.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/daemon/RequestContext.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.daemon; import android.util.Log; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/daemon/VMEventHandler.java b/app/src/main/java/cn/classfun/droidvm/lib/daemon/VMEventHandler.java index da8923c46..19f32545d 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/daemon/VMEventHandler.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/daemon/VMEventHandler.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.daemon; import static android.content.Context.MODE_PRIVATE; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/data/CrosvmExit.java b/app/src/main/java/cn/classfun/droidvm/lib/data/CrosvmExit.java index db4920b34..15913e50e 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/data/CrosvmExit.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/data/CrosvmExit.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.data; /** diff --git a/app/src/main/java/cn/classfun/droidvm/lib/data/Images.java b/app/src/main/java/cn/classfun/droidvm/lib/data/Images.java index 29985ee44..d5c84de1a 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/data/Images.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/data/Images.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.data; import static android.os.Build.SUPPORTED_ABIS; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/data/Language.java b/app/src/main/java/cn/classfun/droidvm/lib/data/Language.java index 1305c2a63..bd62411ac 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/data/Language.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/data/Language.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.data; import static cn.classfun.droidvm.lib.utils.AssetUtils.loadYAMLFromAssets; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/data/License.java b/app/src/main/java/cn/classfun/droidvm/lib/data/License.java index 51726f2a0..2bae82c72 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/data/License.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/data/License.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.data; import static cn.classfun.droidvm.lib.utils.AssetUtils.loadYAMLFromAssets; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/data/QcomChipName.java b/app/src/main/java/cn/classfun/droidvm/lib/data/QcomChipName.java index 851f814ee..135490392 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/data/QcomChipName.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/data/QcomChipName.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.data; import static cn.classfun.droidvm.lib.utils.RunUtils.runListQuiet; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/data/QcomGunyahSupports.java b/app/src/main/java/cn/classfun/droidvm/lib/data/QcomGunyahSupports.java index e1a5fff38..f15f06613 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/data/QcomGunyahSupports.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/data/QcomGunyahSupports.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.data; import static cn.classfun.droidvm.lib.utils.AssetUtils.loadYAMLFromAssets; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/data/Repos.java b/app/src/main/java/cn/classfun/droidvm/lib/data/Repos.java index bf7577f87..c31177db6 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/data/Repos.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/data/Repos.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.data; import static java.util.Objects.requireNonNull; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/diag/LogHelper.java b/app/src/main/java/cn/classfun/droidvm/lib/diag/LogHelper.java index 35c61338d..e0bf52f89 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/diag/LogHelper.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/diag/LogHelper.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.diag; import android.os.Handler; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/diag/LogHelperHandler.java b/app/src/main/java/cn/classfun/droidvm/lib/diag/LogHelperHandler.java index e551bbcc7..96f95ace6 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/diag/LogHelperHandler.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/diag/LogHelperHandler.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.diag; import static android.content.Intent.ACTION_VIEW; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/diag/handler/BadSM8650HostKernelHandler.java b/app/src/main/java/cn/classfun/droidvm/lib/diag/handler/BadSM8650HostKernelHandler.java index 31f220796..a38248bf9 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/diag/handler/BadSM8650HostKernelHandler.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/diag/handler/BadSM8650HostKernelHandler.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.diag.handler; import android.content.Context; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/diag/handler/HugePageFaultHandler.java b/app/src/main/java/cn/classfun/droidvm/lib/diag/handler/HugePageFaultHandler.java index cc69844f7..7bc35bedf 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/diag/handler/HugePageFaultHandler.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/diag/handler/HugePageFaultHandler.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.diag.handler; import android.content.Context; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/diag/handler/OsKernelWithoutRestrictPoolHandler.java b/app/src/main/java/cn/classfun/droidvm/lib/diag/handler/OsKernelWithoutRestrictPoolHandler.java index 5316f4840..2dc9b2bda 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/diag/handler/OsKernelWithoutRestrictPoolHandler.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/diag/handler/OsKernelWithoutRestrictPoolHandler.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.diag.handler; import android.content.Context; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/diag/handler/UnsupportedGunyahVersionHandler.java b/app/src/main/java/cn/classfun/droidvm/lib/diag/handler/UnsupportedGunyahVersionHandler.java index 8c55a0d60..72e1147cf 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/diag/handler/UnsupportedGunyahVersionHandler.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/diag/handler/UnsupportedGunyahVersionHandler.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.diag.handler; import android.content.Context; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/diag/handler/UnsupportedSandboxHandler.java b/app/src/main/java/cn/classfun/droidvm/lib/diag/handler/UnsupportedSandboxHandler.java index 8a870273e..85c64734a 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/diag/handler/UnsupportedSandboxHandler.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/diag/handler/UnsupportedSandboxHandler.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.diag.handler; import android.content.Context; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/download/DiskDownloadManager.java b/app/src/main/java/cn/classfun/droidvm/lib/download/DiskDownloadManager.java index 0d2ff6b21..85b3ccdd1 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/download/DiskDownloadManager.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/download/DiskDownloadManager.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.download; import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/download/DiskDownloadService.java b/app/src/main/java/cn/classfun/droidvm/lib/download/DiskDownloadService.java index 6ea6a7022..494c8c20a 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/download/DiskDownloadService.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/download/DiskDownloadService.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.download; import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/natives/NativeProcess.java b/app/src/main/java/cn/classfun/droidvm/lib/natives/NativeProcess.java index 9d43a59cb..9781a3c8f 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/natives/NativeProcess.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/natives/NativeProcess.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.natives; import static java.lang.Thread.sleep; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/natives/UnixHelper.java b/app/src/main/java/cn/classfun/droidvm/lib/natives/UnixHelper.java index ce3616769..787f07201 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/natives/UnixHelper.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/natives/UnixHelper.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.natives; import static cn.classfun.droidvm.lib.Constants.DATA_DIR; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/network/FDSocket.java b/app/src/main/java/cn/classfun/droidvm/lib/network/FDSocket.java index 731ab3745..894617d44 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/network/FDSocket.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/network/FDSocket.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.network; import android.os.ParcelFileDescriptor; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/network/IPAddress.java b/app/src/main/java/cn/classfun/droidvm/lib/network/IPAddress.java index 283c03bef..090e9a994 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/network/IPAddress.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/network/IPAddress.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.network; import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/network/IPNetwork.java b/app/src/main/java/cn/classfun/droidvm/lib/network/IPNetwork.java index ad3ac87d6..6df9a3587 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/network/IPNetwork.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/network/IPNetwork.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.network; import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/network/IPv4Address.java b/app/src/main/java/cn/classfun/droidvm/lib/network/IPv4Address.java index 90c58aa39..0e05fd4b0 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/network/IPv4Address.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/network/IPv4Address.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.network; import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/network/IPv4Network.java b/app/src/main/java/cn/classfun/droidvm/lib/network/IPv4Network.java index e293d2663..8f81d1ac3 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/network/IPv4Network.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/network/IPv4Network.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.network; import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/network/IPv6Address.java b/app/src/main/java/cn/classfun/droidvm/lib/network/IPv6Address.java index 21b6c52cc..3fe34f9e8 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/network/IPv6Address.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/network/IPv6Address.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.network; import static java.lang.System.arraycopy; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/network/IPv6Network.java b/app/src/main/java/cn/classfun/droidvm/lib/network/IPv6Network.java index 34b0918f7..6d154c003 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/network/IPv6Network.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/network/IPv6Network.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.network; import static cn.classfun.droidvm.lib.network.IPv6Address.MAX_VALUE; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/pkg/BootFile.java b/app/src/main/java/cn/classfun/droidvm/lib/pkg/BootFile.java index 70ece34ec..8b22d0d1d 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/pkg/BootFile.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/pkg/BootFile.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.pkg; import androidx.annotation.NonNull; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/pkg/DiskEntry.java b/app/src/main/java/cn/classfun/droidvm/lib/pkg/DiskEntry.java index 0017d0e6c..24c57c5e5 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/pkg/DiskEntry.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/pkg/DiskEntry.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.pkg; import static cn.classfun.droidvm.lib.store.enums.Enums.optEnum; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/pkg/DiskRef.java b/app/src/main/java/cn/classfun/droidvm/lib/pkg/DiskRef.java index 768b98763..94f0de5bd 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/pkg/DiskRef.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/pkg/DiskRef.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.pkg; import static java.util.Objects.requireNonNull; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/pkg/PackageConstants.java b/app/src/main/java/cn/classfun/droidvm/lib/pkg/PackageConstants.java index c45a8af77..bda4ca68e 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/pkg/PackageConstants.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/pkg/PackageConstants.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.pkg; import cn.classfun.droidvm.lib.archive.Compression; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/pkg/PackageHeader.java b/app/src/main/java/cn/classfun/droidvm/lib/pkg/PackageHeader.java index 64956cf52..2e2b69ae8 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/pkg/PackageHeader.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/pkg/PackageHeader.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.pkg; import static java.nio.charset.StandardCharsets.UTF_8; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/pkg/PackageInput.java b/app/src/main/java/cn/classfun/droidvm/lib/pkg/PackageInput.java index 397c2b1e5..daac35f29 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/pkg/PackageInput.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/pkg/PackageInput.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.pkg; import static cn.classfun.droidvm.lib.archive.TarWriter.wrapCompressionInput; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/pkg/PackageManifest.java b/app/src/main/java/cn/classfun/droidvm/lib/pkg/PackageManifest.java index eaa900961..b20ece300 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/pkg/PackageManifest.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/pkg/PackageManifest.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.pkg; import static cn.classfun.droidvm.lib.store.enums.Enums.optEnum; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/pkg/Phase.java b/app/src/main/java/cn/classfun/droidvm/lib/pkg/Phase.java index 41646fe3b..ce3400d61 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/pkg/Phase.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/pkg/Phase.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.pkg; public enum Phase { diff --git a/app/src/main/java/cn/classfun/droidvm/lib/run/RunContext.java b/app/src/main/java/cn/classfun/droidvm/lib/run/RunContext.java index 86c806445..2ede507e4 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/run/RunContext.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/run/RunContext.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.run; import static cn.classfun.droidvm.lib.utils.RunUtils.escapedString; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/run/RunResult.java b/app/src/main/java/cn/classfun/droidvm/lib/run/RunResult.java index 4c42bfdfb..f95a1a683 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/run/RunResult.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/run/RunResult.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.run; import android.util.Log; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/run/root/RootRunContext.java b/app/src/main/java/cn/classfun/droidvm/lib/run/root/RootRunContext.java index 9fd450b1e..d9f156917 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/run/root/RootRunContext.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/run/root/RootRunContext.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.run.root; import androidx.annotation.NonNull; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/run/root/RootRunResult.java b/app/src/main/java/cn/classfun/droidvm/lib/run/root/RootRunResult.java index 038492599..e2e350ac6 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/run/root/RootRunResult.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/run/root/RootRunResult.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.run.root; import com.topjohnwu.superuser.Shell; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/run/system/SystemRunContext.java b/app/src/main/java/cn/classfun/droidvm/lib/run/system/SystemRunContext.java index 72e5ed5c6..0325cccf1 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/run/system/SystemRunContext.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/run/system/SystemRunContext.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.run.system; import static java.util.Objects.requireNonNullElse; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/run/system/SystemRunResult.java b/app/src/main/java/cn/classfun/droidvm/lib/run/system/SystemRunResult.java index 0a8a3e64a..103fe830f 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/run/system/SystemRunResult.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/run/system/SystemRunResult.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.run.system; import static cn.classfun.droidvm.lib.utils.RunUtils.outStringToList; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/size/SizeNumber.java b/app/src/main/java/cn/classfun/droidvm/lib/size/SizeNumber.java index bc3ef75da..8cf230d58 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/size/SizeNumber.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/size/SizeNumber.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.size; import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/size/SizeUnit.java b/app/src/main/java/cn/classfun/droidvm/lib/size/SizeUnit.java index 4e482d881..50f627ad9 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/size/SizeUnit.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/size/SizeUnit.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.size; import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/size/SizeUtils.java b/app/src/main/java/cn/classfun/droidvm/lib/size/SizeUtils.java index b5870fb2d..19e334289 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/size/SizeUtils.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/size/SizeUtils.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.size; import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/base/DataConfig.java b/app/src/main/java/cn/classfun/droidvm/lib/store/base/DataConfig.java index 09ea8351b..cf6d63950 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/base/DataConfig.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/base/DataConfig.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.store.base; import androidx.annotation.NonNull; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/base/DataItem.java b/app/src/main/java/cn/classfun/droidvm/lib/store/base/DataItem.java index e1de91471..18bda4f29 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/base/DataItem.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/base/DataItem.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.store.base; import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/base/DataStore.java b/app/src/main/java/cn/classfun/droidvm/lib/store/base/DataStore.java index 87a1949a1..4284dee3b 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/base/DataStore.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/base/DataStore.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.store.base; import static cn.classfun.droidvm.lib.utils.FileUtils.loadJSONFile; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/base/JSONSerialize.java b/app/src/main/java/cn/classfun/droidvm/lib/store/base/JSONSerialize.java index 89625af07..fff224285 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/base/JSONSerialize.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/base/JSONSerialize.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.store.base; import androidx.annotation.NonNull; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/base/RingBuffer.java b/app/src/main/java/cn/classfun/droidvm/lib/store/base/RingBuffer.java index 95e541104..1b65fd1ef 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/base/RingBuffer.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/base/RingBuffer.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.store.base; import androidx.annotation.NonNull; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/disk/DiskBus.java b/app/src/main/java/cn/classfun/droidvm/lib/store/disk/DiskBus.java index f9bd3b99b..0491b8c37 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/disk/DiskBus.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/disk/DiskBus.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.store.disk; import androidx.annotation.NonNull; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/disk/DiskConfig.java b/app/src/main/java/cn/classfun/droidvm/lib/store/disk/DiskConfig.java index cc8ac0fda..79b9cdf06 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/disk/DiskConfig.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/disk/DiskConfig.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.store.disk; import static cn.classfun.droidvm.lib.utils.StringUtils.pathJoin; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/disk/DiskStore.java b/app/src/main/java/cn/classfun/droidvm/lib/store/disk/DiskStore.java index 1dce22b4b..17c1cdf8c 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/disk/DiskStore.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/disk/DiskStore.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.store.disk; import android.content.Context; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/enums/ColorEnum.java b/app/src/main/java/cn/classfun/droidvm/lib/store/enums/ColorEnum.java index 4832cc1d6..28d2abd14 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/enums/ColorEnum.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/enums/ColorEnum.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.store.enums; import android.content.Context; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/enums/EnumPicker.java b/app/src/main/java/cn/classfun/droidvm/lib/store/enums/EnumPicker.java index 11af07116..e8cd851db 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/enums/EnumPicker.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/enums/EnumPicker.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.store.enums; import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/enums/Enums.java b/app/src/main/java/cn/classfun/droidvm/lib/store/enums/Enums.java index 5918898f3..251a0969a 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/enums/Enums.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/enums/Enums.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.store.enums; import android.widget.TextView; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/enums/StringEnum.java b/app/src/main/java/cn/classfun/droidvm/lib/store/enums/StringEnum.java index dea25264f..72d419283 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/enums/StringEnum.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/enums/StringEnum.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.store.enums; import android.content.Context; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/network/BridgeType.java b/app/src/main/java/cn/classfun/droidvm/lib/store/network/BridgeType.java index bd8f10da2..1fa5e26bd 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/network/BridgeType.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/network/BridgeType.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.store.network; import androidx.annotation.NonNull; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/network/Ipv6Source.java b/app/src/main/java/cn/classfun/droidvm/lib/store/network/Ipv6Source.java index e27728bb2..fae941086 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/network/Ipv6Source.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/network/Ipv6Source.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.store.network; import androidx.annotation.NonNull; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/network/NetworkConfig.java b/app/src/main/java/cn/classfun/droidvm/lib/store/network/NetworkConfig.java index d44e27afc..9ce832721 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/network/NetworkConfig.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/network/NetworkConfig.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.store.network; import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/network/NetworkConfigValidator.java b/app/src/main/java/cn/classfun/droidvm/lib/store/network/NetworkConfigValidator.java index 8ba24cfc8..7b31d5b4d 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/network/NetworkConfigValidator.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/network/NetworkConfigValidator.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.store.network; import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/network/NetworkState.java b/app/src/main/java/cn/classfun/droidvm/lib/store/network/NetworkState.java index e46628c1c..ff565547c 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/network/NetworkState.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/network/NetworkState.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.store.network; import androidx.annotation.ColorRes; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/network/NetworkStore.java b/app/src/main/java/cn/classfun/droidvm/lib/store/network/NetworkStore.java index b2b7f7d83..6e160edbb 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/network/NetworkStore.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/network/NetworkStore.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.store.network; import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/network/UplinkMode.java b/app/src/main/java/cn/classfun/droidvm/lib/store/network/UplinkMode.java index b5a6d0136..cd0468e32 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/network/UplinkMode.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/network/UplinkMode.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.store.network; import androidx.annotation.NonNull; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/network/VlanConfig.java b/app/src/main/java/cn/classfun/droidvm/lib/store/network/VlanConfig.java index 8bf15f154..ca4161439 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/network/VlanConfig.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/network/VlanConfig.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.store.network; import androidx.annotation.NonNull; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/BootConfig.java b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/BootConfig.java index 97a0f551b..d0f79e194 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/BootConfig.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/BootConfig.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.store.vm; import androidx.annotation.NonNull; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/DisplayBackend.java b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/DisplayBackend.java index c8d912409..6e1ed58e5 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/DisplayBackend.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/DisplayBackend.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.store.vm; import androidx.annotation.StringRes; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/GpuApi.java b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/GpuApi.java index fb4494118..49cc5d623 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/GpuApi.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/GpuApi.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.store.vm; import androidx.annotation.StringRes; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/GpuBackend.java b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/GpuBackend.java index c8628da8c..cb5eb7ccc 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/GpuBackend.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/GpuBackend.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.store.vm; import androidx.annotation.StringRes; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/LendMthpMode.java b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/LendMthpMode.java index d45676c48..2cf7e70d9 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/LendMthpMode.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/LendMthpMode.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.store.vm; import static cn.classfun.droidvm.lib.store.enums.Enums.optEnum; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/NativeDisplay.java b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/NativeDisplay.java index b04457605..4e211b600 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/NativeDisplay.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/NativeDisplay.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.store.vm; import static cn.classfun.droidvm.lib.Constants.DATA_DIR; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/PortProtocol.java b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/PortProtocol.java index edca416f7..0f39edfd4 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/PortProtocol.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/PortProtocol.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.store.vm; import androidx.annotation.NonNull; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/ProtectedVM.java b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/ProtectedVM.java index 49f2866b0..95632dd33 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/ProtectedVM.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/ProtectedVM.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.store.vm; import androidx.annotation.StringRes; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/SharedDirCache.java b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/SharedDirCache.java index 03baec596..da921faff 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/SharedDirCache.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/SharedDirCache.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.store.vm; import androidx.annotation.NonNull; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/SharedDirType.java b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/SharedDirType.java index 9f34edfd8..ec6bf2890 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/SharedDirType.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/SharedDirType.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.store.vm; import androidx.annotation.NonNull; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMBackend.java b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMBackend.java index f40c452d7..96fd0e77d 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMBackend.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMBackend.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.store.vm; import androidx.annotation.StringRes; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMConfig.java b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMConfig.java index 4ef282125..deff965f4 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMConfig.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMConfig.java @@ -1,6 +1,13 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.store.vm; import static cn.classfun.droidvm.lib.Constants.PATH_EDK2_FIRMWARE; +import static cn.classfun.droidvm.lib.Constants.PATH_BUILTIN_INITRD; +import static cn.classfun.droidvm.lib.Constants.PATH_BUILTIN_KERNEL; + +import android.content.Context; import androidx.annotation.NonNull; @@ -15,6 +22,14 @@ import cn.classfun.droidvm.lib.store.base.DataItem; public class VMConfig extends DataConfig { + public static final boolean NEW_VM_DEFAULT_HUGEPAGES = true; + public static final boolean NEW_VM_DEFAULT_PMU = true; + public static final boolean NEW_VM_DEFAULT_RNG = true; + public static final boolean NEW_VM_DEFAULT_SMT = true; + public static final boolean NEW_VM_DEFAULT_USB = true; + public static final ProtectedVM NEW_VM_DEFAULT_PROTECTED_VM = + ProtectedVM.PSEUDO_UNPROTECTED; + public VMConfig() { setId(UUID.randomUUID()); item.set("created_at", System.currentTimeMillis()); @@ -28,6 +43,101 @@ public VMConfig(@NonNull JSONObject obj) throws JSONException { } migrateBoot(); migrateNicForwards(); + // Configs from before "screens" describe one display chosen by an either/or backend + // enum; fold that into the per-screen bindings, dropping the legacy keys so nothing + // downstream can read a second, disagreeing answer. + VMScreenConfig.migrate(item); + // Configs from before "serial_ports" implicitly meant "COM1 = app console, rest sinks"; + // make that explicit so every reader sees the same list. + VMSerialConfig.ensureDefaults(item); + } + + /** + * Materializes the values shown by every tab when Customize opens for a new VM. Quick + * creation starts here and only replaces the fields it exposes, so both creation paths keep + * the same defaults as those defaults evolve. + */ + @NonNull + public static VMConfig createWithCustomizeDefaults(@NonNull Context context) { + var config = new VMConfig(); + var item = config.item; + item.set("memory_mb", 512L); + item.set("cpu_count", 1L); + item.set("swiotlb_mb", 256L); + item.set("balloon", false); + item.set("pmu", NEW_VM_DEFAULT_PMU); + item.set("rng", NEW_VM_DEFAULT_RNG); + item.set("smt", NEW_VM_DEFAULT_SMT); + item.set("usb", NEW_VM_DEFAULT_USB); + item.set("sandbox", false); + item.set("hugepages", NEW_VM_DEFAULT_HUGEPAGES); + item.set("strace", false); + item.set("gpu_vram_folio_threshold_kb", 1024L); + item.set(LendMthpMode.KEY, LendMthpMode.defaultForDevice(context)); + item.set("protected_vm", NEW_VM_DEFAULT_PROTECTED_VM); + item.set("backend", VMBackend.DEFAULT); + item.set("hypervisor", VMHypervisor.defaultForNewVm(VMBackend.DEFAULT)); + item.set("extra_options", DataItem.newArray()); + item.set("environment_variables", DataItem.newArray()); + item.set(CpuPlacementPlan.KEY_AFFINITY, ""); + item.set(CpuPlacementPlan.KEY_AUTO, true); + item.set(CpuPlacementPlan.KEY_CAPACITY, ""); + item.set(CpuPlacementPlan.KEY_CLUSTERS, ""); + + var boot = BootConfig.of(config); + boot.setProtocol(BootConfig.Protocol.UEFI); + boot.setUefiFirmware(""); + boot.setUefiVarsEnabled(true); + boot.setUefiVars(""); + boot.setLinuxSource(BootConfig.LinuxSource.MANUAL); + boot.setKernel(PATH_BUILTIN_KERNEL); + boot.setInitrd(PATH_BUILTIN_INITRD); + boot.setCmdline(BootConfig.DEFAULT_MANUAL_CMDLINE); + boot.setImageCmdline(""); + boot.setImageDisk(0); + boot.setVdafix(true); + boot.setBootWait(BootConfig.DEFAULT_BOOT_WAIT); + item.set("auto_up", false); + + item.set("disks", DataItem.newArray()); + item.set("shared_dirs", DataItem.newArray()); + item.set("networks", DataItem.newArray()); + + var gpu = VMScreenConfig.of(item, VMScreenConfig.ID_GPU0); + gpu.setEnabled(false); + gpu.setExporter(DisplayExporter.NONE); + gpu.setTransportCap(DisplayTransportCap.defaultFor( + VMScreenConfig.ID_GPU0, DisplayExporter.NATIVE)); + gpu.setInputEnabled(true); + gpu.setWidth(VMScreenConfig.DEFAULT_WIDTH); + gpu.setHeight(VMScreenConfig.DEFAULT_HEIGHT); + gpu.setRefreshRate(VMScreenConfig.DEFAULT_REFRESH_RATE); + gpu.setDpiH(VMScreenConfig.DEFAULT_DPI); + gpu.setDpiV(VMScreenConfig.DEFAULT_DPI); + + var simpleFb = VMScreenConfig.of(item, VMScreenConfig.ID_SIMPLEFB); + simpleFb.setEnabled(true); + simpleFb.setExporter(DisplayExporter.NATIVE); + simpleFb.setTransportCap(DisplayTransportCap.defaultFor( + VMScreenConfig.ID_SIMPLEFB, DisplayExporter.NATIVE)); + simpleFb.setInputEnabled(true); + simpleFb.setWidth(VMScreenConfig.DEFAULT_WIDTH); + simpleFb.setHeight(VMScreenConfig.DEFAULT_HEIGHT); + simpleFb.setPollHz(VMScreenConfig.NEW_VM_DEFAULT_POLL_HZ); + item.set("display_blit_provider", GpuBlitProvider.TURNIP); + item.set(CpuPlacementPlan.KEY_GPU_CGROUP, false); + item.set(CpuPlacementPlan.KEY_GPU_CGROUP_PATH, + CpuPlacementPlan.DEFAULT_GPU_CGROUP_PATH); + item.set(CpuPlacementPlan.KEY_GPU_CGROUP_CPUS, ""); + VpuConfig.setEnabled(item, false); + VpuConfig.setHostPoolMb(item, VpuConfig.DEFAULT_HOST_POOL_MB); + VpuConfig.setGuestPoolMb(item, VpuConfig.DEFAULT_GUEST_POOL_MB); + + var peripherals = DataItem.newArray(); + peripherals.append(VMPeripheralConfig.createDefaultVirtioSound().item); + item.set("peripherals", peripherals); + VMSerialConfig.ensureDefaults(item); + return config; } /** diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMHypervisor.java b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMHypervisor.java index e592d5007..8b91dc74b 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMHypervisor.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMHypervisor.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.store.vm; import static cn.classfun.droidvm.lib.utils.FileUtils.shellCheckExists; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMNicConfig.java b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMNicConfig.java index d3be02f9a..44cfc51f9 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMNicConfig.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMNicConfig.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.store.vm; import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMState.java b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMState.java index 291ff6859..7c127e266 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMState.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMState.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.store.vm; import androidx.annotation.ColorRes; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMStore.java b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMStore.java index 06dd953d1..dcf25f41d 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMStore.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMStore.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.store.vm; import android.content.Context; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/ui/BackAskHelper.java b/app/src/main/java/cn/classfun/droidvm/lib/ui/BackAskHelper.java index e725862ed..1e422b989 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/ui/BackAskHelper.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/ui/BackAskHelper.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.ui; import androidx.activity.OnBackPressedCallback; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/ui/CopyableField.java b/app/src/main/java/cn/classfun/droidvm/lib/ui/CopyableField.java index 2c7dcb84d..ae48272ae 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/ui/CopyableField.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/ui/CopyableField.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.ui; import android.content.ClipData; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/ui/DragTouchListener.java b/app/src/main/java/cn/classfun/droidvm/lib/ui/DragTouchListener.java index 67c2d5e76..34e46dd31 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/ui/DragTouchListener.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/ui/DragTouchListener.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.ui; import static java.lang.Math.hypot; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/ui/IconItemAdapter.java b/app/src/main/java/cn/classfun/droidvm/lib/ui/IconItemAdapter.java index 2a62a972c..51ecc3a2c 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/ui/IconItemAdapter.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/ui/IconItemAdapter.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.ui; import android.content.Context; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/ui/ImeInsetsApplier.java b/app/src/main/java/cn/classfun/droidvm/lib/ui/ImeInsetsApplier.java index 948b49525..20532c39b 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/ui/ImeInsetsApplier.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/ui/ImeInsetsApplier.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.ui; import android.app.Activity; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/ui/ImeInsetsExempt.java b/app/src/main/java/cn/classfun/droidvm/lib/ui/ImeInsetsExempt.java index a78ec638e..c20b7b6e6 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/ui/ImeInsetsExempt.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/ui/ImeInsetsExempt.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.ui; /** diff --git a/app/src/main/java/cn/classfun/droidvm/lib/ui/MaterialMenu.java b/app/src/main/java/cn/classfun/droidvm/lib/ui/MaterialMenu.java index 2ac024dea..c3b32e6fc 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/ui/MaterialMenu.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/ui/MaterialMenu.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.ui; import android.content.Context; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/ui/MenuDialogBuilder.java b/app/src/main/java/cn/classfun/droidvm/lib/ui/MenuDialogBuilder.java index f7abdbf2b..f2e6dc21d 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/ui/MenuDialogBuilder.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/ui/MenuDialogBuilder.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.ui; import android.content.Context; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/ui/NotificationPermission.java b/app/src/main/java/cn/classfun/droidvm/lib/ui/NotificationPermission.java index 1d7d4878d..64ecbb2e8 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/ui/NotificationPermission.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/ui/NotificationPermission.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.ui; import android.Manifest; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/ui/SimpleAdapterDataObserver.java b/app/src/main/java/cn/classfun/droidvm/lib/ui/SimpleAdapterDataObserver.java index 314e58b84..e7c73a188 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/ui/SimpleAdapterDataObserver.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/ui/SimpleAdapterDataObserver.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.ui; import androidx.annotation.NonNull; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/ui/SimpleTextWatcher.java b/app/src/main/java/cn/classfun/droidvm/lib/ui/SimpleTextWatcher.java index 2fcaae356..6bb3b4e6e 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/ui/SimpleTextWatcher.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/ui/SimpleTextWatcher.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.ui; import android.text.Editable; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/ui/SwipeableTabActivity.java b/app/src/main/java/cn/classfun/droidvm/lib/ui/SwipeableTabActivity.java index 2cd6710db..dc4042043 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/ui/SwipeableTabActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/ui/SwipeableTabActivity.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.ui; import android.view.MotionEvent; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/ui/TabSwipeHelper.java b/app/src/main/java/cn/classfun/droidvm/lib/ui/TabSwipeHelper.java index ff415d81c..48ef75cd5 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/ui/TabSwipeHelper.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/ui/TabSwipeHelper.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.ui; import android.app.Activity; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/ui/UIContext.java b/app/src/main/java/cn/classfun/droidvm/lib/ui/UIContext.java index b7cd64bf3..f070de281 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/ui/UIContext.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/ui/UIContext.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.ui; import android.app.Activity; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/ui/termux/SimpleTerminalSessionClient.java b/app/src/main/java/cn/classfun/droidvm/lib/ui/termux/SimpleTerminalSessionClient.java index 07e471c3e..1c612c02d 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/ui/termux/SimpleTerminalSessionClient.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/ui/termux/SimpleTerminalSessionClient.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.ui.termux; import android.content.ClipboardManager; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/ui/termux/SimpleTerminalViewClient.java b/app/src/main/java/cn/classfun/droidvm/lib/ui/termux/SimpleTerminalViewClient.java index 0bcbf66a9..9e6d101f0 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/ui/termux/SimpleTerminalViewClient.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/ui/termux/SimpleTerminalViewClient.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.ui.termux; import android.util.Log; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/ui/termux/TerminalFonts.java b/app/src/main/java/cn/classfun/droidvm/lib/ui/termux/TerminalFonts.java index d6b64d9e2..3ec7c0157 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/ui/termux/TerminalFonts.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/ui/termux/TerminalFonts.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.ui.termux; import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/utils/AssetUtils.java b/app/src/main/java/cn/classfun/droidvm/lib/utils/AssetUtils.java index 6ca1f30f1..e1604e0a1 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/utils/AssetUtils.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/utils/AssetUtils.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.utils; import static android.os.Build.SUPPORTED_ABIS; @@ -292,8 +295,28 @@ public static void cleanupPrebuilt(@NonNull Context context) { } } + /** + * Serialises extraction. This writes into the data dir, so two callers at once are two + * writers interleaving mkdir and file creation on the same tree -- observed as one thread + * failing on "Failed to mkdir .../usr/bin" while the other completed, two milliseconds + * apart. The up-to-date check inside the lock is what makes the second caller cheap: by the + * time it gets in, the work is done and it returns immediately. + * + *

    Held here rather than at each call site because "only one extraction at a time" is a + * property of the operation, not something every caller should have to remember. + */ + private static final Object EXTRACT_LOCK = new Object(); + public static void extractPrebuilt( @NonNull Context context + ) throws IOException, JSONException { + synchronized (EXTRACT_LOCK) { + extractPrebuiltLocked(context); + } + } + + private static void extractPrebuiltLocked( + @NonNull Context context ) throws IOException, JSONException { if (!needsExtractPrebuilt(context)) { Log.d(TAG, "Prebuilt archive is up to date, skipping extraction"); diff --git a/app/src/main/java/cn/classfun/droidvm/lib/utils/BinaryUtils.java b/app/src/main/java/cn/classfun/droidvm/lib/utils/BinaryUtils.java index fb1039c63..2c1869654 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/utils/BinaryUtils.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/utils/BinaryUtils.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.utils; import static java.nio.charset.StandardCharsets.UTF_8; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/utils/CpuUtils.java b/app/src/main/java/cn/classfun/droidvm/lib/utils/CpuUtils.java index 571414a45..155a34137 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/utils/CpuUtils.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/utils/CpuUtils.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.utils; import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/utils/FileUtils.java b/app/src/main/java/cn/classfun/droidvm/lib/utils/FileUtils.java index 6f9075f46..69d017c8d 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/utils/FileUtils.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/utils/FileUtils.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.utils; import static cn.classfun.droidvm.lib.utils.RunUtils.escapedString; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/utils/ImageUtils.java b/app/src/main/java/cn/classfun/droidvm/lib/utils/ImageUtils.java index ba4d3042c..4e48ff6d6 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/utils/ImageUtils.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/utils/ImageUtils.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.utils; import static cn.classfun.droidvm.lib.utils.AssetUtils.getPrebuiltBinaryPath; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/utils/JsonUtils.java b/app/src/main/java/cn/classfun/droidvm/lib/utils/JsonUtils.java index 86d11638e..58d9319b0 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/utils/JsonUtils.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/utils/JsonUtils.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.utils; import androidx.annotation.NonNull; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/utils/NetUtils.java b/app/src/main/java/cn/classfun/droidvm/lib/utils/NetUtils.java index f4506352c..313594019 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/utils/NetUtils.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/utils/NetUtils.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.utils; import static org.yaml.snakeyaml.util.UriEncoder.encode; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/utils/ProcessUtils.java b/app/src/main/java/cn/classfun/droidvm/lib/utils/ProcessUtils.java index 698e3da14..7e99eec03 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/utils/ProcessUtils.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/utils/ProcessUtils.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.utils; import static java.lang.Integer.parseInt; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/utils/RunUtils.java b/app/src/main/java/cn/classfun/droidvm/lib/utils/RunUtils.java index 31b56dfa4..c30f1476e 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/utils/RunUtils.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/utils/RunUtils.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.utils; import static java.util.Objects.requireNonNullElse; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/utils/ShareUtils.java b/app/src/main/java/cn/classfun/droidvm/lib/utils/ShareUtils.java index ee824e39a..1f94c1a53 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/utils/ShareUtils.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/utils/ShareUtils.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.utils; import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/utils/StringUtils.java b/app/src/main/java/cn/classfun/droidvm/lib/utils/StringUtils.java index 649dbc101..3b6608c40 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/utils/StringUtils.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/utils/StringUtils.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.utils; import static java.nio.charset.StandardCharsets.UTF_8; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/utils/ThreadUtils.java b/app/src/main/java/cn/classfun/droidvm/lib/utils/ThreadUtils.java index 3c47dbe7d..a77adbb6a 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/utils/ThreadUtils.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/utils/ThreadUtils.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.utils; import static java.util.concurrent.Executors.newCachedThreadPool; diff --git a/app/src/main/java/cn/classfun/droidvm/lib/utils/Try.java b/app/src/main/java/cn/classfun/droidvm/lib/utils/Try.java index ed195bbb4..c6a030239 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/utils/Try.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/utils/Try.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.utils; import androidx.annotation.Nullable; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/SplashActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/SplashActivity.java index efaf37367..9300c8ef4 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/SplashActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/SplashActivity.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui; import static cn.classfun.droidvm.lib.utils.AssetUtils.extractBinaries; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/agent/AgentOperationActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/agent/AgentOperationActivity.java index 83f4b35be..8b600e21d 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/agent/AgentOperationActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/agent/AgentOperationActivity.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.agent; import static android.view.View.GONE; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/agent/base/AgentVM.java b/app/src/main/java/cn/classfun/droidvm/ui/agent/base/AgentVM.java index 0b4ca11e5..95eb357f7 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/agent/base/AgentVM.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/agent/base/AgentVM.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.agent.base; import static cn.classfun.droidvm.lib.Constants.DATA_DIR; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/agent/base/BaseAction.java b/app/src/main/java/cn/classfun/droidvm/ui/agent/base/BaseAction.java index 61e75783f..9a112ac55 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/agent/base/BaseAction.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/agent/base/BaseAction.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.agent.base; import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/agent/password/ChangePasswordActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/agent/password/ChangePasswordActivity.java index 0e68865af..93194d372 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/agent/password/ChangePasswordActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/agent/password/ChangePasswordActivity.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.agent.password; import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/agent/password/PasswordAction.java b/app/src/main/java/cn/classfun/droidvm/ui/agent/password/PasswordAction.java index 5c0683c16..f5475ac33 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/agent/password/PasswordAction.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/agent/password/PasswordAction.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.agent.password; import androidx.annotation.NonNull; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/disk/action/DiskActionDialog.java b/app/src/main/java/cn/classfun/droidvm/ui/disk/action/DiskActionDialog.java index 99d717265..d0c37defa 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/disk/action/DiskActionDialog.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/disk/action/DiskActionDialog.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.disk.action; import static android.widget.Toast.LENGTH_SHORT; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/disk/action/DiskCloneDialog.java b/app/src/main/java/cn/classfun/droidvm/ui/disk/action/DiskCloneDialog.java index e64c8100f..24406b1c6 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/disk/action/DiskCloneDialog.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/disk/action/DiskCloneDialog.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.disk.action; import static cn.classfun.droidvm.lib.utils.FileUtils.shellCheckExists; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/disk/action/DiskResizeDialog.java b/app/src/main/java/cn/classfun/droidvm/ui/disk/action/DiskResizeDialog.java index d4a2b9c82..d0f14d0a3 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/disk/action/DiskResizeDialog.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/disk/action/DiskResizeDialog.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.disk.action; import static cn.classfun.droidvm.lib.utils.ThreadUtils.runOnPool; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/disk/action/DiskSetFormatDialog.java b/app/src/main/java/cn/classfun/droidvm/ui/disk/action/DiskSetFormatDialog.java index 358f48696..98688864d 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/disk/action/DiskSetFormatDialog.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/disk/action/DiskSetFormatDialog.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.disk.action; import static android.widget.Toast.LENGTH_LONG; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/disk/create/DiskCompress.java b/app/src/main/java/cn/classfun/droidvm/ui/disk/create/DiskCompress.java index 855fb1106..ce409a809 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/disk/create/DiskCompress.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/disk/create/DiskCompress.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.disk.create; import androidx.annotation.StringRes; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/disk/create/DiskCreateActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/disk/create/DiskCreateActivity.java index b372f6bc2..fa3a0b82b 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/disk/create/DiskCreateActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/disk/create/DiskCreateActivity.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.disk.create; import static android.view.View.GONE; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/disk/create/DiskFormat.java b/app/src/main/java/cn/classfun/droidvm/ui/disk/create/DiskFormat.java index 7d1945d7b..7a45f26ea 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/disk/create/DiskFormat.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/disk/create/DiskFormat.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.disk.create; import static cn.classfun.droidvm.lib.utils.StringUtils.extensionLower; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/disk/download/ImportURLActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/disk/download/ImportURLActivity.java index a6cacee16..b81133c85 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/disk/download/ImportURLActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/disk/download/ImportURLActivity.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.disk.download; import static android.view.View.GONE; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/disk/images/FlatImage.java b/app/src/main/java/cn/classfun/droidvm/ui/disk/images/FlatImage.java index cbf01d2a2..ff56efea5 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/disk/images/FlatImage.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/disk/images/FlatImage.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.disk.images; import static cn.classfun.droidvm.lib.size.SizeUtils.formatSize; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/disk/images/ImageAdapter.java b/app/src/main/java/cn/classfun/droidvm/ui/disk/images/ImageAdapter.java index c6ab065ec..cf1ac9f13 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/disk/images/ImageAdapter.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/disk/images/ImageAdapter.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.disk.images; import android.annotation.SuppressLint; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/disk/images/ImagePickerDialog.java b/app/src/main/java/cn/classfun/droidvm/ui/disk/images/ImagePickerDialog.java index c2de92862..7285257cc 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/disk/images/ImagePickerDialog.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/disk/images/ImagePickerDialog.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.disk.images; import static android.view.View.GONE; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/disk/images/ImageViewHolder.java b/app/src/main/java/cn/classfun/droidvm/ui/disk/images/ImageViewHolder.java index e975d0061..5720ddee3 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/disk/images/ImageViewHolder.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/disk/images/ImageViewHolder.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.disk.images; import android.view.View; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/disk/images/ImportImagesActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/disk/images/ImportImagesActivity.java index 389b809e4..213f354e9 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/disk/images/ImportImagesActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/disk/images/ImportImagesActivity.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.disk.images; import static android.view.View.GONE; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/disk/info/DiskInfoActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/disk/info/DiskInfoActivity.java index d48909210..7e4b17f85 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/disk/info/DiskInfoActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/disk/info/DiskInfoActivity.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.disk.info; import static cn.classfun.droidvm.lib.utils.AssetUtils.getPrebuiltBinaryPath; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/disk/info/base/DiskInfoBaseTab.java b/app/src/main/java/cn/classfun/droidvm/ui/disk/info/base/DiskInfoBaseTab.java index 3df8212de..2b7e27fec 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/disk/info/base/DiskInfoBaseTab.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/disk/info/base/DiskInfoBaseTab.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.disk.info.base; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/disk/info/base/DiskInfoTab.java b/app/src/main/java/cn/classfun/droidvm/ui/disk/info/base/DiskInfoTab.java index 19997997d..3c65236cb 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/disk/info/base/DiskInfoTab.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/disk/info/base/DiskInfoTab.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.disk.info.base; import android.widget.FrameLayout; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/disk/info/info/DiskInfoInfoTab.java b/app/src/main/java/cn/classfun/droidvm/ui/disk/info/info/DiskInfoInfoTab.java index f819a5845..40e1e49bd 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/disk/info/info/DiskInfoInfoTab.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/disk/info/info/DiskInfoInfoTab.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.disk.info.info; import static android.view.View.GONE; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/disk/info/snapshot/DiskInfoSnapshotTab.java b/app/src/main/java/cn/classfun/droidvm/ui/disk/info/snapshot/DiskInfoSnapshotTab.java index 465057d97..55eba1e6a 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/disk/info/snapshot/DiskInfoSnapshotTab.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/disk/info/snapshot/DiskInfoSnapshotTab.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.disk.info.snapshot; import static android.content.DialogInterface.BUTTON_POSITIVE; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/disk/info/snapshot/DiskSnapshotEntryAdapter.java b/app/src/main/java/cn/classfun/droidvm/ui/disk/info/snapshot/DiskSnapshotEntryAdapter.java index 0066b2da1..2499caffb 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/disk/info/snapshot/DiskSnapshotEntryAdapter.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/disk/info/snapshot/DiskSnapshotEntryAdapter.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.disk.info.snapshot; import static android.view.View.GONE; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/disk/info/snapshot/DiskSnapshotEntryViewHolder.java b/app/src/main/java/cn/classfun/droidvm/ui/disk/info/snapshot/DiskSnapshotEntryViewHolder.java index fc492c61e..63e160732 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/disk/info/snapshot/DiskSnapshotEntryViewHolder.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/disk/info/snapshot/DiskSnapshotEntryViewHolder.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.disk.info.snapshot; import android.view.View; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/disk/info/tree/DiskInfoTreeTab.java b/app/src/main/java/cn/classfun/droidvm/ui/disk/info/tree/DiskInfoTreeTab.java index 39e3dce05..08bf40046 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/disk/info/tree/DiskInfoTreeTab.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/disk/info/tree/DiskInfoTreeTab.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.disk.info.tree; import static android.view.View.GONE; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/disk/info/tree/DiskTreeEntryAdapter.java b/app/src/main/java/cn/classfun/droidvm/ui/disk/info/tree/DiskTreeEntryAdapter.java index 45c1f8d69..f78a2c7dc 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/disk/info/tree/DiskTreeEntryAdapter.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/disk/info/tree/DiskTreeEntryAdapter.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.disk.info.tree; import static android.view.View.GONE; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/disk/info/tree/DiskTreeEntryViewHolder.java b/app/src/main/java/cn/classfun/droidvm/ui/disk/info/tree/DiskTreeEntryViewHolder.java index 8a62760e7..0f341cbd7 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/disk/info/tree/DiskTreeEntryViewHolder.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/disk/info/tree/DiskTreeEntryViewHolder.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.disk.info.tree; import android.view.View; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/disk/lxc/ImportLxcImagesActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/disk/lxc/ImportLxcImagesActivity.java index ebe80ea9b..6e0ea17a7 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/disk/lxc/ImportLxcImagesActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/disk/lxc/ImportLxcImagesActivity.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.disk.lxc; import static android.R.attr.colorError; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/disk/lxc/LxcImage.java b/app/src/main/java/cn/classfun/droidvm/ui/disk/lxc/LxcImage.java index 6054d46b5..28a17a620 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/disk/lxc/LxcImage.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/disk/lxc/LxcImage.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.disk.lxc; import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/disk/lxc/LxcImageParser.java b/app/src/main/java/cn/classfun/droidvm/ui/disk/lxc/LxcImageParser.java index c5159e512..0f2392c0a 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/disk/lxc/LxcImageParser.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/disk/lxc/LxcImageParser.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.disk.lxc; import static android.os.Build.SUPPORTED_ABIS; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/disk/operation/DiskOperationActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/disk/operation/DiskOperationActivity.java index 6edf43ac9..7c0e51f99 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/disk/operation/DiskOperationActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/disk/operation/DiskOperationActivity.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.disk.operation; import static android.view.View.GONE; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/disk/operation/ImageCommandGenerate.java b/app/src/main/java/cn/classfun/droidvm/ui/disk/operation/ImageCommandGenerate.java index 825285c46..87df93805 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/disk/operation/ImageCommandGenerate.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/disk/operation/ImageCommandGenerate.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.disk.operation; import static cn.classfun.droidvm.lib.utils.AssetUtils.getPrebuiltBinaryPath; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/AcquireContainerView.java b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/AcquireContainerView.java index 1a56bf0a5..ea0d5fa1c 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/AcquireContainerView.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/AcquireContainerView.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.hugepage; import android.content.Context; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageActivity.java index 261e1c59f..1ccc5bfc5 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageActivity.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.hugepage; import static android.view.View.GONE; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageColor.java b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageColor.java index 7287fbef9..a19f34a9d 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageColor.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageColor.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.hugepage; import android.content.Context; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageModel.java b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageModel.java index aef1fb615..fc331b324 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageModel.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageModel.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.hugepage; import static cn.classfun.droidvm.lib.utils.FileUtils.shellCheckExists; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcess.java b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcess.java index 406faec50..e953bce18 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcess.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcess.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.hugepage; import androidx.annotation.NonNull; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcessActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcessActivity.java index 53f4e9809..dccc4d350 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcessActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcessActivity.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.hugepage; import static android.view.View.GONE; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcessAdapter.java b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcessAdapter.java index 1e3206493..51fe99236 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcessAdapter.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcessAdapter.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.hugepage; import static android.view.View.GONE; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/SegmentedBar.java b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/SegmentedBar.java index e81bcd868..5888c11df 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/hugepage/SegmentedBar.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/hugepage/SegmentedBar.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.hugepage; import android.content.Context; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/logs/LogAdapter.java b/app/src/main/java/cn/classfun/droidvm/ui/logs/LogAdapter.java index 94ba80d7c..23b1468a3 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/logs/LogAdapter.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/logs/LogAdapter.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.logs; import android.view.LayoutInflater; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/logs/LogViewHolder.java b/app/src/main/java/cn/classfun/droidvm/ui/logs/LogViewHolder.java index 3b39c8755..7cfbb66c4 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/logs/LogViewHolder.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/logs/LogViewHolder.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.logs; import android.view.View; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/logs/LogsActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/logs/LogsActivity.java index 0292a8946..5b10d255d 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/logs/LogsActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/logs/LogsActivity.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.logs; import static java.nio.charset.StandardCharsets.UTF_8; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/main/MainActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/main/MainActivity.java index 26144f4f5..85cdb2630 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/main/MainActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/main/MainActivity.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.main; import static android.content.Intent.ACTION_VIEW; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/main/base/BaseViewHolder.java b/app/src/main/java/cn/classfun/droidvm/ui/main/base/BaseViewHolder.java index e99d11f94..fe9e9c611 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/main/base/BaseViewHolder.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/main/base/BaseViewHolder.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.main.base; import android.view.View; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/main/base/MainBaseFragment.java b/app/src/main/java/cn/classfun/droidvm/ui/main/base/MainBaseFragment.java index d746e1a99..583012f58 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/main/base/MainBaseFragment.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/main/base/MainBaseFragment.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.main.base; import android.os.Bundle; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/main/base/MainFragmentEnum.java b/app/src/main/java/cn/classfun/droidvm/ui/main/base/MainFragmentEnum.java index 6f8f0a1d5..c5ed53159 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/main/base/MainFragmentEnum.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/main/base/MainFragmentEnum.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.main.base; import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/main/base/list/DataAdapter.java b/app/src/main/java/cn/classfun/droidvm/ui/main/base/list/DataAdapter.java index 4aa236459..c4c882500 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/main/base/list/DataAdapter.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/main/base/list/DataAdapter.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.main.base.list; import android.annotation.SuppressLint; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/main/base/list/MainListFragment.java b/app/src/main/java/cn/classfun/droidvm/ui/main/base/list/MainListFragment.java index b64ac47e8..7b673f056 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/main/base/list/MainListFragment.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/main/base/list/MainListFragment.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.main.base.list; import static android.view.View.GONE; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/main/base/stateful/MainStatefulFragment.java b/app/src/main/java/cn/classfun/droidvm/ui/main/base/stateful/MainStatefulFragment.java index 3e6cb949d..d76b3c9d5 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/main/base/stateful/MainStatefulFragment.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/main/base/stateful/MainStatefulFragment.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.main.base.stateful; import static cn.classfun.droidvm.lib.store.enums.Enums.optEnum; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/main/base/stateful/StatefulAdapter.java b/app/src/main/java/cn/classfun/droidvm/ui/main/base/stateful/StatefulAdapter.java index 15392970c..3fd4f5344 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/main/base/stateful/StatefulAdapter.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/main/base/stateful/StatefulAdapter.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.main.base.stateful; import static android.view.View.VISIBLE; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/main/disk/DiskAdapter.java b/app/src/main/java/cn/classfun/droidvm/ui/main/disk/DiskAdapter.java index e97e932b4..e2355af39 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/main/disk/DiskAdapter.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/main/disk/DiskAdapter.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.main.disk; import static android.view.View.VISIBLE; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/main/disk/ImageInfo.java b/app/src/main/java/cn/classfun/droidvm/ui/main/disk/ImageInfo.java index 3085297ff..f1f64f5ea 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/main/disk/ImageInfo.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/main/disk/ImageInfo.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.main.disk; final class ImageInfo { diff --git a/app/src/main/java/cn/classfun/droidvm/ui/main/disk/MainDiskFragment.java b/app/src/main/java/cn/classfun/droidvm/ui/main/disk/MainDiskFragment.java index f4174e07b..a472f6689 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/main/disk/MainDiskFragment.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/main/disk/MainDiskFragment.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.main.disk; import android.app.Activity; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/main/home/MainHomeFragment.java b/app/src/main/java/cn/classfun/droidvm/ui/main/home/MainHomeFragment.java index 4eaedd551..3bb466986 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/main/home/MainHomeFragment.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/main/home/MainHomeFragment.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.main.home; import static android.content.Intent.ACTION_VIEW; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/main/network/MainNetworkFragment.java b/app/src/main/java/cn/classfun/droidvm/ui/main/network/MainNetworkFragment.java index c340258f6..4f13bdaf3 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/main/network/MainNetworkFragment.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/main/network/MainNetworkFragment.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.main.network; import static android.widget.Toast.LENGTH_LONG; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/main/network/NetworkAdapter.java b/app/src/main/java/cn/classfun/droidvm/ui/main/network/NetworkAdapter.java index 5aeb08da1..d02eb48bd 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/main/network/NetworkAdapter.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/main/network/NetworkAdapter.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.main.network; import static android.view.View.VISIBLE; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/main/settings/ApiManagerDialog.java b/app/src/main/java/cn/classfun/droidvm/ui/main/settings/ApiManagerDialog.java index e70b43af9..f2e565cdb 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/main/settings/ApiManagerDialog.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/main/settings/ApiManagerDialog.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.main.settings; import android.content.Context; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/main/settings/ApiServiceAdapter.java b/app/src/main/java/cn/classfun/droidvm/ui/main/settings/ApiServiceAdapter.java index 7632fe3a7..48d1ab6ad 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/main/settings/ApiServiceAdapter.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/main/settings/ApiServiceAdapter.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.main.settings; import android.view.LayoutInflater; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/main/settings/ApiServiceViewHolder.java b/app/src/main/java/cn/classfun/droidvm/ui/main/settings/ApiServiceViewHolder.java index 8b35ee3cb..4ffd0c272 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/main/settings/ApiServiceViewHolder.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/main/settings/ApiServiceViewHolder.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.main.settings; import android.view.View; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/main/settings/LicenseListAdapter.java b/app/src/main/java/cn/classfun/droidvm/ui/main/settings/LicenseListAdapter.java index 5f7949bb3..434a1328f 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/main/settings/LicenseListAdapter.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/main/settings/LicenseListAdapter.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.main.settings; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/main/settings/MainSettingsFragment.java b/app/src/main/java/cn/classfun/droidvm/ui/main/settings/MainSettingsFragment.java index 617628e24..0ae19e041 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/main/settings/MainSettingsFragment.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/main/settings/MainSettingsFragment.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.main.settings; import static android.content.Intent.ACTION_VIEW; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/main/settings/SoftwareLicenseDialog.java b/app/src/main/java/cn/classfun/droidvm/ui/main/settings/SoftwareLicenseDialog.java index 3ef82c42d..c6442c8b3 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/main/settings/SoftwareLicenseDialog.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/main/settings/SoftwareLicenseDialog.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.main.settings; import static android.content.Intent.ACTION_VIEW; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/main/vm/MainVMFragment.java b/app/src/main/java/cn/classfun/droidvm/ui/main/vm/MainVMFragment.java index 0d79233c3..dc8f0a984 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/main/vm/MainVMFragment.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/main/vm/MainVMFragment.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.main.vm; import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/main/vm/VMAdapter.java b/app/src/main/java/cn/classfun/droidvm/ui/main/vm/VMAdapter.java index 29a66d326..06fcb5b8e 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/main/vm/VMAdapter.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/main/vm/VMAdapter.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.main.vm; import static android.view.View.VISIBLE; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/network/NetworkActions.java b/app/src/main/java/cn/classfun/droidvm/ui/network/NetworkActions.java index 4e8a43830..8329e2233 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/network/NetworkActions.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/network/NetworkActions.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.network; import static android.widget.Toast.LENGTH_LONG; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/network/edit/NetworkEditActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/network/edit/NetworkEditActivity.java index 4ba17b646..71d6a5053 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/network/edit/NetworkEditActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/network/edit/NetworkEditActivity.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.network.edit; import static android.view.View.GONE; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/network/edit/VlanCardBinder.java b/app/src/main/java/cn/classfun/droidvm/ui/network/edit/VlanCardBinder.java index 5e640ea7d..3c5080032 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/network/edit/VlanCardBinder.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/network/edit/VlanCardBinder.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.network.edit; import static android.view.View.GONE; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/network/info/NetworkInfoActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/network/info/NetworkInfoActivity.java index 3e4149902..f6262f680 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/network/info/NetworkInfoActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/network/info/NetworkInfoActivity.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.network.info; import static android.view.View.GONE; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/network/info/NetworkToolLogActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/network/info/NetworkToolLogActivity.java index 2659a6a23..dd0b4665c 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/network/info/NetworkToolLogActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/network/info/NetworkToolLogActivity.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.network.info; import android.os.Bundle; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/setup/SetupActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/setup/SetupActivity.java index b419f57b8..b470c09b5 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/setup/SetupActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/setup/SetupActivity.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.setup; import static android.view.View.GONE; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/setup/base/BaseCheckStepFragment.java b/app/src/main/java/cn/classfun/droidvm/ui/setup/base/BaseCheckStepFragment.java index cbd35647c..99e907573 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/setup/base/BaseCheckStepFragment.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/setup/base/BaseCheckStepFragment.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.setup.base; import static android.view.View.GONE; @@ -33,9 +36,25 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat runCheck(); } + /** + * onViewCreated has already run the check by the time the first onResume arrives, and every + * runCheck() here starts a worker thread. Re-running it immediately means two workers doing + * the same job: harmless for the read-only steps, but the extract step writes files, and two + * of those racing produced "extraction failed" followed by "extraction succeeded" -- the + * loser tripping over the directories the winner was creating. + * + *

    Later resumes still re-check, which is the point of doing it here at all: the user + * leaves to grant root or a permission and comes back expecting the step to notice. + */ + private boolean resumedBefore; + @Override public void onResume() { super.onResume(); + if (!resumedBefore) { + resumedBefore = true; + return; + } runCheck(); } diff --git a/app/src/main/java/cn/classfun/droidvm/ui/setup/base/BaseStepFragment.java b/app/src/main/java/cn/classfun/droidvm/ui/setup/base/BaseStepFragment.java index e24414663..c0f750734 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/setup/base/BaseStepFragment.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/setup/base/BaseStepFragment.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.setup.base; import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/setup/step/DoneStepFragment.java b/app/src/main/java/cn/classfun/droidvm/ui/setup/step/DoneStepFragment.java index 226ea9d82..699a52e6c 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/setup/step/DoneStepFragment.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/setup/step/DoneStepFragment.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.setup.step; import static android.content.Context.MODE_PRIVATE; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/setup/step/ExtractStepFragment.java b/app/src/main/java/cn/classfun/droidvm/ui/setup/step/ExtractStepFragment.java index 23afa2efb..18a5d0951 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/setup/step/ExtractStepFragment.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/setup/step/ExtractStepFragment.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.setup.step; import static cn.classfun.droidvm.lib.utils.AssetUtils.extractPrebuilt; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/setup/step/PrivacyStepFragment.java b/app/src/main/java/cn/classfun/droidvm/ui/setup/step/PrivacyStepFragment.java index 2c61ab2b2..d9400c569 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/setup/step/PrivacyStepFragment.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/setup/step/PrivacyStepFragment.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.setup.step; import android.os.Bundle; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/setup/step/RootStepFragment.java b/app/src/main/java/cn/classfun/droidvm/ui/setup/step/RootStepFragment.java index a03eb10f7..f1193f7b7 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/setup/step/RootStepFragment.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/setup/step/RootStepFragment.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.setup.step; import static cn.classfun.droidvm.lib.utils.RunUtils.runList; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/setup/step/SocStepFragment.java b/app/src/main/java/cn/classfun/droidvm/ui/setup/step/SocStepFragment.java index 4e5f20ce9..454bc4a1e 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/setup/step/SocStepFragment.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/setup/step/SocStepFragment.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.setup.step; import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/setup/step/StartStepFragment.java b/app/src/main/java/cn/classfun/droidvm/ui/setup/step/StartStepFragment.java index 3457dd9dc..788bf5b5f 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/setup/step/StartStepFragment.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/setup/step/StartStepFragment.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.setup.step; import static android.view.View.GONE; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/setup/step/StorageStepFragment.java b/app/src/main/java/cn/classfun/droidvm/ui/setup/step/StorageStepFragment.java index 1a81188bf..0a910f4ed 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/setup/step/StorageStepFragment.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/setup/step/StorageStepFragment.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.setup.step; import static android.view.View.GONE; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/setup/step/VirtualizationStepFragment.java b/app/src/main/java/cn/classfun/droidvm/ui/setup/step/VirtualizationStepFragment.java index 5003a803b..46c07a99b 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/setup/step/VirtualizationStepFragment.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/setup/step/VirtualizationStepFragment.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.setup.step; import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/update/UpdateDialog.java b/app/src/main/java/cn/classfun/droidvm/ui/update/UpdateDialog.java index 907b0df57..24a47af0e 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/update/UpdateDialog.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/update/UpdateDialog.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.update; import android.content.Context; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/update/UpdateInfo.java b/app/src/main/java/cn/classfun/droidvm/ui/update/UpdateInfo.java index b3544e101..39b3cc5d9 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/update/UpdateInfo.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/update/UpdateInfo.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.update; import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/update/VersionCheck.java b/app/src/main/java/cn/classfun/droidvm/ui/update/VersionCheck.java index e9e0ea393..9a13bfc8b 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/update/VersionCheck.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/update/VersionCheck.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.update; import static cn.classfun.droidvm.lib.utils.ThreadUtils.runOnPool; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/NicLeaseAllocator.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/NicLeaseAllocator.java index 018fb1b34..208175004 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/NicLeaseAllocator.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/NicLeaseAllocator.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.vm; import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/VMActions.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/VMActions.java index 8804b5a88..ab59a5748 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/VMActions.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/VMActions.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.vm; import static android.widget.Toast.LENGTH_LONG; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/boot/BootEntries.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/boot/BootEntries.java index de833127f..7728040bc 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/boot/BootEntries.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/boot/BootEntries.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.vm.boot; import static cn.classfun.droidvm.lib.utils.StringUtils.pathJoin; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/boot/BootMenuDialog.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/boot/BootMenuDialog.java index cd84b5bb4..6d919be51 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/boot/BootMenuDialog.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/boot/BootMenuDialog.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.vm.boot; import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/console/VMConsoleActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/console/VMConsoleActivity.java index 12fcaba1a..2dec5a72a 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/console/VMConsoleActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/console/VMConsoleActivity.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.vm.console; import static android.view.HapticFeedbackConstants.KEYBOARD_TAP; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/console/VMConsoleRouter.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/console/VMConsoleRouter.java index a8a014634..b23567a87 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/console/VMConsoleRouter.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/console/VMConsoleRouter.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.vm.console; import android.content.Context; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/BaseExtraKeysAdapter.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/BaseExtraKeysAdapter.java index f0cc647e3..295d48b2f 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/BaseExtraKeysAdapter.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/BaseExtraKeysAdapter.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.vm.display.base; import static android.view.KeyEvent.KEYCODE_ALT_LEFT; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/DisplayExtraKeysPanel.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/DisplayExtraKeysPanel.java index 6cd012460..2152723e4 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/DisplayExtraKeysPanel.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/DisplayExtraKeysPanel.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.vm.display.base; import static android.view.HapticFeedbackConstants.KEYBOARD_TAP; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/DisplayPresentation.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/DisplayPresentation.java index 434f28478..d0f76954f 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/DisplayPresentation.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/DisplayPresentation.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.vm.display.base; import android.app.Presentation; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/DisplayTouchPadPanel.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/DisplayTouchPadPanel.java index 5f8233bdb..30526ae63 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/DisplayTouchPadPanel.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/DisplayTouchPadPanel.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.vm.display.base; import static android.view.HapticFeedbackConstants.KEYBOARD_TAP; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/KeyListener.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/KeyListener.java index 79433a4f9..48dc5a234 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/KeyListener.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/KeyListener.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.vm.display.base; public interface KeyListener { diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/UsbHidInput.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/UsbHidInput.java index d034ee04a..d445fd231 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/UsbHidInput.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/UsbHidInput.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.vm.display.base; import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/X11Keymap.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/X11Keymap.java index 22921d5e1..37c5e8457 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/X11Keymap.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/X11Keymap.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.vm.display.base; import android.view.KeyEvent; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/display/DisplayProvider.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/display/DisplayProvider.java index 524e5db1b..f5e85ea4e 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/display/DisplayProvider.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/display/DisplayProvider.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.vm.display.nativedisplay.display; import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/display/VMNativeDisplayActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/display/VMNativeDisplayActivity.java index 12f0622f2..a6209ade8 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/display/VMNativeDisplayActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/display/VMNativeDisplayActivity.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.vm.display.nativedisplay.display; import static android.view.Gravity.CENTER; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/DirectInputSink.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/DirectInputSink.java index 523c758a5..f03f98155 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/DirectInputSink.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/DirectInputSink.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.vm.display.nativedisplay.input; import static cn.classfun.droidvm.lib.store.vm.NativeDisplay.CHANNEL_COUNT; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/EvdevEncoder.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/EvdevEncoder.java index a78b2d2b8..9f3d6bc5c 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/EvdevEncoder.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/EvdevEncoder.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.vm.display.nativedisplay.input; import android.view.MotionEvent; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/InputForwarder.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/InputForwarder.java index ce192c4a7..ef001df42 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/InputForwarder.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/InputForwarder.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.vm.display.nativedisplay.input; import static cn.classfun.droidvm.lib.store.vm.NativeDisplay.KEYBOARD; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/KeyCodeMapper.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/KeyCodeMapper.java index 644eaf138..22b3a0e24 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/KeyCodeMapper.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/KeyCodeMapper.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.vm.display.nativedisplay.input; import android.view.KeyEvent; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/NativeExtraKeysPanel.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/NativeExtraKeysPanel.java index 3cbe10f8b..f85f30eff 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/NativeExtraKeysPanel.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/NativeExtraKeysPanel.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.vm.display.nativedisplay.input; import static android.view.KeyEvent.KEYCODE_CAPS_LOCK; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/NativeKeyboardEditText.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/NativeKeyboardEditText.java index d7ef0de4e..68abe6f8c 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/NativeKeyboardEditText.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/NativeKeyboardEditText.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.vm.display.nativedisplay.input; import android.content.Context; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/TouchScaleCalculator.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/TouchScaleCalculator.java index 85a91bc43..e677bf071 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/TouchScaleCalculator.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/TouchScaleCalculator.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.vm.display.nativedisplay.input; /** diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/base/BaseVncActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/base/BaseVncActivity.java index e5447a5d7..4b44ca8ee 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/base/BaseVncActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/base/BaseVncActivity.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.vm.display.vnc.base; import static android.content.DialogInterface.BUTTON_NEUTRAL; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/base/VncClient.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/base/VncClient.java index 3975fc088..4e490374d 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/base/VncClient.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/base/VncClient.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.vm.display.vnc.base; import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/base/VncDisplayView.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/base/VncDisplayView.java index 863a8d86f..fefb4c29e 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/base/VncDisplayView.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/base/VncDisplayView.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.vm.display.vnc.base; import android.content.Context; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/display/VMVncDisplayActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/display/VMVncDisplayActivity.java index e177de753..135310e7d 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/display/VMVncDisplayActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/display/VMVncDisplayActivity.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.vm.display.vnc.display; import static android.view.Gravity.BOTTOM; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/display/VMVncPresentationActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/display/VMVncPresentationActivity.java index f76cfa08a..1f7231175 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/display/VMVncPresentationActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/display/VMVncPresentationActivity.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.vm.display.vnc.display; import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/input/VncExtraKeysPanel.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/input/VncExtraKeysPanel.java index 0f2c440dd..2bc4c3649 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/input/VncExtraKeysPanel.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/input/VncExtraKeysPanel.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.vm.display.vnc.input; import static android.view.KeyEvent.KEYCODE_CAPS_LOCK; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/input/VncTouchPadPanel.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/input/VncTouchPadPanel.java index b9e99b1bc..925170a28 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/input/VncTouchPadPanel.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/input/VncTouchPadPanel.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.vm.display.vnc.input; import static java.lang.Math.max; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/VMEditActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/VMEditActivity.java index acb06d0f9..c540d5d1d 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/VMEditActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/VMEditActivity.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.vm.edit; import static java.util.Objects.requireNonNull; @@ -27,11 +30,14 @@ import cn.classfun.droidvm.R; import cn.classfun.droidvm.lib.ui.BackAskHelper; +import cn.classfun.droidvm.lib.ui.CameraPermission; +import cn.classfun.droidvm.lib.ui.RecordAudioPermission; import cn.classfun.droidvm.lib.ui.SwipeableTabActivity; import cn.classfun.droidvm.lib.store.vm.VMConfig; import cn.classfun.droidvm.lib.store.vm.VMStore; import cn.classfun.droidvm.ui.vm.edit.base.VMEditBaseTab; import cn.classfun.droidvm.ui.vm.edit.base.VMEditTab; +import cn.classfun.droidvm.ui.vm.edit.peripheral.VMEditPeripheralTab; public final class VMEditActivity extends SwipeableTabActivity { private static final String TAG = "VMEditActivity"; @@ -47,6 +53,10 @@ public final class VMEditActivity extends SwipeableTabActivity { public Consumer currentPicker = null; public boolean editMode = false; public UUID editVMId = null; + private RecordAudioPermission recordAudioPermission; + private CameraPermission cameraPermission; + /** A microphone permission choice has already been offered during this edit session. */ + private boolean micPermissionHandled = false; private final Map sharedData = new HashMap<>(); @@ -59,6 +69,25 @@ public T get(@NonNull String key, T def) { return (T) sharedData.getOrDefault(key, def); } + /** Host mic permission gate, shared with the peripheral tab. */ + @NonNull + public RecordAudioPermission getRecordAudioPermission() { + return recordAudioPermission; + } + + /** Shares one microphone permission prompt between adding a device and saving the VM. */ + public void ensureRecordAudioThen(@NonNull Runnable action) { + micPermissionHandled = true; + recordAudioPermission.ensureThen(action); + } + + /** Host camera permission gate, shared with the peripheral tab. Unlike the mic one, a + * refusal here stops the peripheral being added -- see {@link CameraPermission}. */ + @NonNull + public CameraPermission getCameraPermission() { + return cameraPermission; + } + private void pickerResult(Uri uri) { if (currentPicker != null && uri != null) currentPicker.accept(uri); @@ -75,6 +104,10 @@ private void folderPickerResult(Uri uri) { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_vm_edit); + // Registered here, ahead of the tabs: an ActivityResultLauncher can only be created + // before the activity reaches STARTED. + recordAudioPermission = new RecordAudioPermission(this); + cameraPermission = new CameraPermission(this); tabs = createTabInstances(); collapsingToolbar = findViewById(R.id.collapsing_toolbar); tabLayout = findViewById(R.id.tab_layout); @@ -159,7 +192,14 @@ private void initialize() { fab.setOnClickListener(v -> doSave()); setupTabs(); tabs.forEach(VMEditBaseTab::initValue); + // Both paths load a config. Without this a new VM showed whatever android:text the + // layout happened to carry, so every numeric field had two defaults -- the XML one the + // user actually got, and the optLong fallback that only applied when editing an older + // VM missing that key. They had already drifted: the DRM pool offered 1024 in the form + // while the code said 8, and nothing could notice, because each is only reachable from + // a path the other never takes. if (editMode) loadExistingConfig(); + else tabs.forEach(tab -> tab.loadConfig(VMConfig.createWithCustomizeDefaults(this))); } private void loadExistingConfig() { @@ -182,6 +222,13 @@ public List createTabInstances() { } private void doSave() { + var peripheralTab = getTab(VMEditTab.TAB_PERIPHERAL); + if (!micPermissionHandled + && peripheralTab instanceof VMEditPeripheralTab + && ((VMEditPeripheralTab) peripheralTab).hasMicrophone()) { + ensureRecordAudioThen(this::doSave); + return; + } // Commit any field still being edited: NIC MAC/offset/forward inputs // write back to the model on focus loss, so flush the focused view // before reading the tabs' config. @@ -220,7 +267,7 @@ private void doSave() { return; } } else { - config = new VMConfig(); + config = VMConfig.createWithCustomizeDefaults(this); } for (var tab : tabs) { try { diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/base/VMEditBaseTab.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/base/VMEditBaseTab.java index a5fba6a9c..90b1f82d3 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/base/VMEditBaseTab.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/base/VMEditBaseTab.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.vm.edit.base; import android.view.View; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/base/VMEditTab.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/base/VMEditTab.java index 93a936821..5668040a5 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/base/VMEditTab.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/base/VMEditTab.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.vm.edit.base; import static java.util.Objects.requireNonNull; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/basic/VMEditBasicTab.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/basic/VMEditBasicTab.java index 31ce2dc2c..a20aa4cbb 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/basic/VMEditBasicTab.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/basic/VMEditBasicTab.java @@ -1,12 +1,17 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.vm.edit.basic; +import static android.view.View.GONE; +import static android.view.View.VISIBLE; +import static java.lang.Integer.parseInt; import static cn.classfun.droidvm.lib.utils.FileUtils.checkFileName; import static cn.classfun.droidvm.lib.store.enums.Enums.optEnum; import static cn.classfun.droidvm.lib.store.vm.ProtectedVM.PROTECTED_WITHOUT_FIRMWARE; import static cn.classfun.droidvm.lib.utils.StringUtils.getEditText; import android.text.TextUtils; -import android.util.Log; import android.view.View; import androidx.annotation.NonNull; @@ -14,10 +19,15 @@ import com.google.android.material.textfield.TextInputEditText; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + import cn.classfun.droidvm.R; -import cn.classfun.droidvm.lib.data.QcomChipName; -import cn.classfun.droidvm.lib.data.QcomGunyahSupports; import cn.classfun.droidvm.lib.store.base.DataItem; +import cn.classfun.droidvm.lib.store.vm.CpuPlacementDraft; +import cn.classfun.droidvm.lib.store.vm.CpuPlacementPlan; import cn.classfun.droidvm.lib.store.vm.LendMthpMode; import cn.classfun.droidvm.lib.store.vm.ProtectedVM; import cn.classfun.droidvm.lib.store.vm.VMBackend; @@ -25,14 +35,17 @@ import cn.classfun.droidvm.lib.store.vm.VMConfig; import cn.classfun.droidvm.lib.store.vm.VMHypervisor; import cn.classfun.droidvm.lib.store.vm.VMStore; +import cn.classfun.droidvm.lib.utils.CpuUtils; import cn.classfun.droidvm.ui.vm.edit.VMEditActivity; import cn.classfun.droidvm.ui.vm.edit.base.VMEditBaseTab; import cn.classfun.droidvm.ui.widgets.row.ChooseRowWidget; import cn.classfun.droidvm.ui.widgets.row.SwitchRowWidget; import cn.classfun.droidvm.ui.widgets.row.TextInputRowWidget; +import cn.classfun.droidvm.ui.widgets.tools.CpuCorePickerDialog; public final class VMEditBasicTab extends VMEditBaseTab { - private final String TAG = "VMEditBasicTab"; + /** Matches the ti_max on input_cpu in partial_vm_edit_basic.xml. */ + private static final int MAX_VCPUS = 64; private TextInputRowWidget inputName; private TextInputRowWidget inputMemory; private TextInputRowWidget inputCpu; @@ -50,6 +63,21 @@ public final class VMEditBasicTab extends VMEditBaseTab { private ChooseRowWidget chooseBackend; private ChooseRowWidget chooseHypervisor; private TextInputEditText etExtraOptions; + private TextInputEditText etEnvironmentVariables; + + /** Host cores as reported by sysfs; read once, drives the cpuset picker. */ + private List hostCores = List.of(); + /** + * vCPU affinity held for the editor dialog and for save: vCPU index to host + * cores. Only vCPUs the user actually bound appear, matching crosvm's + * "absent means no mask". Edited through {@link VMCpuAffinityDialog}. + */ + private final Map> affinity = new TreeMap<>(); + /** Auto-derive capacity/cluster; the dialog owns this, the tab persists it. */ + private boolean cpuTopologyAuto = true; + /** Manual capacity/cluster overrides, only meaningful when auto is off. */ + private String manualCapacity = ""; + private String manualClusters = ""; public VMEditBasicTab(VMEditActivity parent, View view) { super(parent, view); @@ -74,6 +102,7 @@ public void initView() { chooseBackend = view.findViewById(R.id.choose_backend); chooseHypervisor = view.findViewById(R.id.choose_hypervisor); etExtraOptions = view.findViewById(R.id.et_extra_options); + etEnvironmentVariables = view.findViewById(R.id.et_environment_variables); } @Override @@ -81,27 +110,28 @@ public void initValue() { inputMemory.setValue(512, SizeUnit.MB); inputCpu.setValue(1); inputSwiotlb.setValue(64, SizeUnit.MB); + swBalloon.setChecked(false); + swPmu.setChecked(VMConfig.NEW_VM_DEFAULT_PMU); + swRng.setChecked(VMConfig.NEW_VM_DEFAULT_RNG); + swSmt.setChecked(VMConfig.NEW_VM_DEFAULT_SMT); + swUsb.setChecked(VMConfig.NEW_VM_DEFAULT_USB); + swSandbox.setChecked(false); + swHugepages.setChecked(VMConfig.NEW_VM_DEFAULT_HUGEPAGES); swDebug.setChecked(false); - chooseProtectedVm.configure(ProtectedVM.class, PROTECTED_WITHOUT_FIRMWARE); + chooseProtectedVm.configure( + ProtectedVM.class, VMConfig.NEW_VM_DEFAULT_PROTECTED_VM); chooseBackend.configure(VMBackend.class, VMBackend.DEFAULT); - chooseHypervisor.configure(VMHypervisor.class, VMHypervisor.DEFAULT); - choosePrepareLendMthp.configure(LendMthpMode.class, LendMthpMode.CHUNKED); + chooseHypervisor.configure( + VMHypervisor.class, VMHypervisor.defaultForNewVm(VMBackend.DEFAULT)); + choosePrepareLendMthp.configure( + LendMthpMode.class, LendMthpMode.defaultForDevice(parent)); parent.put("backend", VMBackend.DEFAULT); - parent.put("hypervisor", VMHypervisor.DEFAULT); + parent.put("hypervisor", chooseHypervisor.getSelectedItem()); chooseBackend.setOnValueChangedListener((oldValue, newValue) -> parent.put("backend", newValue)); chooseHypervisor.setOnValueChangedListener((oldValue, newValue) -> parent.put("hypervisor", newValue)); - try { - var socModel = QcomChipName.getCurrentSoC(); - var gunyah = new QcomGunyahSupports(parent); - if (gunyah.isCapacitySupported(socModel, "no_mthp")) - choosePrepareLendMthp.setSelectedItem(LendMthpMode.DISABLED); - if (gunyah.isCapacitySupported(socModel, "mthp_chunked")) - choosePrepareLendMthp.setSelectedItem(LendMthpMode.CHUNKED); - if (gunyah.isCapacitySupported(socModel, "mthp_single")) - choosePrepareLendMthp.setSelectedItem(LendMthpMode.SINGLE); - } catch (Exception e) { - Log.w(TAG, "failed to load soc capacity", e); - } + chooseProtectedVm.setOnValueChangedListener((oldValue, newValue) -> updateProtectedVisibility()); + updateProtectedVisibility(); + initCpuTopology(); } @Override @@ -110,7 +140,7 @@ public void loadConfig(@NonNull VMConfig config) { inputName.setText(config.getName()); inputMemory.setValue(item.optLong("memory_mb", 512), SizeUnit.MB); inputCpu.setValue(item.optLong("cpu_count", 1)); - inputSwiotlb.setValue(item.optLong("swiotlb_mb", 64), SizeUnit.MB); + inputSwiotlb.setValue(item.optLong("swiotlb_mb", 256), SizeUnit.MB); swBalloon.setChecked(item.optBoolean("balloon", false)); swPmu.setChecked(item.optBoolean("pmu", false)); swRng.setChecked(item.optBoolean("rng", false)); @@ -122,7 +152,11 @@ public void loadConfig(@NonNull VMConfig config) { choosePrepareLendMthp.setSelectedItem(LendMthpMode.fromItem(item)); chooseProtectedVm.setSelectedItem(optEnum(item, "protected_vm", PROTECTED_WITHOUT_FIRMWARE)); chooseBackend.setSelectedItem(optEnum(item, "backend", VMBackend.DEFAULT)); - chooseHypervisor.setSelectedItem(optEnum(item, "hypervisor", VMHypervisor.DEFAULT)); + var backend = optEnum(item, "backend", VMBackend.DEFAULT); + var configuredHypervisor = optEnum(item, "hypervisor", VMHypervisor.DEFAULT); + var hypervisor = VMHypervisor.resolveConfigured(backend, configuredHypervisor); + chooseHypervisor.setSelectedItem(hypervisor != null + ? hypervisor : VMHypervisor.defaultForNewVm(backend)); var extraOpts = item.opt("extra_options", null); if (extraOpts != null && extraOpts.is(DataItem.Type.ARRAY)) { var sb = new StringBuilder(); @@ -132,6 +166,123 @@ public void loadConfig(@NonNull VMConfig config) { } etExtraOptions.setText(sb.toString()); } + var environmentVariables = item.opt("environment_variables", null); + if (environmentVariables != null && environmentVariables.is(DataItem.Type.ARRAY)) { + var sb = new StringBuilder(); + for (int i = 0; i < environmentVariables.size(); i++) { + if (i > 0) sb.append('\n'); + sb.append(environmentVariables.optString(i, "")); + } + etEnvironmentVariables.setText(sb.toString()); + } else { + etEnvironmentVariables.setText(""); + } + loadCpuTopology(item); + updateProtectedVisibility(); + } + + private void loadCpuTopology(@NonNull DataItem item) { + affinity.clear(); + affinity.putAll(CpuPlacementPlan.parseAffinity( + item.optString(CpuPlacementPlan.KEY_AFFINITY, ""))); + cpuTopologyAuto = item.optBoolean(CpuPlacementPlan.KEY_AUTO, true); + manualCapacity = item.optString(CpuPlacementPlan.KEY_CAPACITY, ""); + manualClusters = item.optString(CpuPlacementPlan.KEY_CLUSTERS, ""); + } + + /** + * Wires up CPU placement: the vCPU affinity editor behind the CPU count field's + * icon button. The affinity, capacity and cluster flags are one decision from + * three sides -- affinity pins vCPU threads to host cores, capacity and cluster + * describe that placement to the guest via the FDT -- so they are edited together + * in {@link VMCpuAffinityDialog}. + */ + private void initCpuTopology() { + hostCores = CpuUtils.getCores(); + inputCpu.setIconButtonOnClickListener(this::showAffinityDialog); + } + + /** + * Opens the affinity editor for the vCPU count as currently entered. Reading + * the count here rather than tracking edits is the point of the dialog: the + * row list cannot disagree with the field. + * + *

    The count travels back the same way, because the dialog's simple mode + * derives it from the host cores that were checked -- one vCPU each. + */ + private void showAffinityDialog() { + var draft = new CpuPlacementDraft(affinity, currentVcpuCount(), + cpuTopologyAuto, manualCapacity, manualClusters); + new VMCpuAffinityDialog(parent, draft, accepted -> { + affinity.clear(); + affinity.putAll(accepted.affinity); + cpuTopologyAuto = accepted.auto; + manualCapacity = accepted.manualCapacity; + manualClusters = accepted.manualClusters; + if (accepted.vcpuCount != currentVcpuCount()) + inputCpu.setValue(accepted.vcpuCount); + }); + } + + // Gunyah dynamic memory sharing is a hypervisor-level memory-sharing mechanism (the GPU is + // just its first user), so it sits with the other lend/share options rather than in the + // graphics tab where it used to live. + /** + * Show the SWIOTLB size only where a VM can use one. + * + * A bounce pool exists because the hypervisor has taken the guest's memory away from the + * host: virtio has to hand the host buffers it can still reach. An unprotected VM never lost + * that access, and a pseudo-unprotected one gets it back before the payload runs, so in both + * the field would ask for memory nothing would ever bounce through -- and in the second it + * would actively hurt, putting a restricted-dma-pool node in the tree of a guest kernel that + * was never built to honour one. The backend ignores the stored value in those modes; this + * keeps the field from claiming otherwise. + */ + private void updateProtectedVisibility() { + var pvm = chooseProtectedVm.getSelectedItem(); + boolean bounces = pvm == ProtectedVM.PROTECTED_PROTECTED + || pvm == ProtectedVM.PROTECTED_WITHOUT_FIRMWARE; + inputSwiotlb.setVisibility(bounces ? VISIBLE : GONE); + } + + /** vCPU count as currently typed, clamped to the field's own 1..64 range. */ + private int currentVcpuCount() { + try { + var text = inputCpu.getText().trim(); + if (text.isEmpty()) return 1; + return Math.max(1, Math.min(parseInt(text), MAX_VCPUS)); + } catch (Exception ignored) { + return 1; + } + } + + @NonNull + private static String joinCsv(@NonNull Collection values) { + var sb = new StringBuilder(); + for (var value : values) { + if (sb.length() > 0) sb.append(','); + sb.append(value); + } + return sb.toString(); + } + + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + private boolean checkInputField( + @NonNull TextInputEditText field, + boolean allowEmpty, int min, int max + ) { + field.setError(null); + try { + var text = getEditText(field); + if (text.isEmpty() && allowEmpty) return true; + var ret = parseInt(text); + if (ret < min || ret > max) + throw new IllegalArgumentException(); + return true; + } catch (Exception ignored) { + field.setError(parent.getString(R.string.create_vm_error_invalid_number)); + return false; + } } private boolean validateInputName(@NonNull VMStore store) { @@ -190,12 +341,81 @@ private boolean validateHypervisor(@NonNull VMStore ignored) { return true; } + private boolean validateEnvironmentVariables() { + etEnvironmentVariables.setError(null); + for (var line : getEditText(etEnvironmentVariables).split("\n")) { + var trimmed = line.trim(); + if (trimmed.isEmpty()) continue; + var separator = trimmed.indexOf('='); + if (separator <= 0 || trimmed.substring(0, separator).trim().isEmpty()) { + etEnvironmentVariables.setError( + parent.getString(R.string.create_vm_error_invalid_environment_variable)); + return false; + } + } + return true; + } + @Override public boolean validateInput(@NonNull VMStore store) { if (!validateInputName(store)) return false; if (!validateInputMemory(store)) return false; if (!validateInputCpu(store)) return false; if (!validateHypervisor(store)) return false; + if (!validateEnvironmentVariables()) return false; + if (!validateCpuTopology()) return false; + return true; + } + + /** + * The affinity dialog already keeps its own edits in range, so this mostly + * guards a stored config that was hand-edited, or a CPU count lowered after + * the affinity was set. + */ + private boolean validateCpuTopology() { + if (!affinity.isEmpty()) { + int count = currentVcpuCount(); + var hostIdx = CpuCorePickerDialog.hostCoreIndices(hostCores); + for (var entry : affinity.entrySet()) { + if (entry.getKey() >= count) + return showValidateFailed(R.string.create_vm_error_cpu_affinity_vcpu_oob); + for (var host : entry.getValue()) + if (!hostIdx.contains(host)) + return showValidateFailed(parent.getString( + R.string.create_vm_error_cpu_affinity_host_oob, host)); + } + if (!cpuTopologyAuto && !validateManualTopology(count)) return false; + } + return true; + } + + /** + * Hand-written capacity/cluster strings. Capacity is only range-checked; + * clusters additionally must not repeat a vCPU, which crosvm rejects + * ("CPU index must be unique"). + */ + private boolean validateManualTopology(int vcpuCount) { + var capacityText = manualCapacity.trim(); + if (!capacityText.isEmpty()) { + var capacity = CpuPlacementPlan.parseCapacity(capacityText); + if (capacity.isEmpty()) + return showValidateFailed(R.string.create_vm_error_cpu_capacity_invalid); + for (var entry : capacity.entrySet()) { + if (entry.getKey() >= vcpuCount) + return showValidateFailed(R.string.create_vm_error_cpu_affinity_vcpu_oob); + if (entry.getValue() > CpuUtils.MAX_CAPACITY) + return showValidateFailed(R.string.create_vm_error_cpu_capacity_invalid); + } + } + var clusters = CpuPlacementPlan.parseClusters(manualClusters.trim()); + var overlaps = CpuPlacementPlan.findClusterOverlaps(clusters); + if (!overlaps.isEmpty()) + return showValidateFailed(parent.getString( + R.string.create_vm_error_cpu_clusters_overlap, joinCsv(overlaps))); + for (var cluster : clusters) + for (var vcpu : cluster) + if (vcpu >= vcpuCount) + return showValidateFailed(R.string.create_vm_error_cpu_affinity_vcpu_oob); return true; } @@ -230,6 +450,34 @@ public void saveConfig(@NonNull VMConfig config) { arr.append(DataItem.newString(trimmed)); } item.set("extra_options", arr); + + var environmentVariables = DataItem.newArray(); + var environmentText = getEditText(etEnvironmentVariables); + for (var line : environmentText.split("\n")) { + var trimmed = line.trim(); + if (!trimmed.isEmpty()) + environmentVariables.append(DataItem.newString(trimmed)); + } + item.set("environment_variables", environmentVariables); + saveCpuTopology(item); + } + + private void saveCpuTopology(@NonNull DataItem item) { + // An empty affinity string is how CpuPlacementPlan is told to emit no CPU + // placement flags at all; the dialog returns an empty map when turned off. + item.set(CpuPlacementPlan.KEY_AFFINITY, CpuPlacementPlan.formatAffinity(affinity)); + item.set(CpuPlacementPlan.KEY_AUTO, cpuTopologyAuto); + item.set(CpuPlacementPlan.KEY_CAPACITY, manualCapacity); + item.set(CpuPlacementPlan.KEY_CLUSTERS, manualClusters); + } + + /** + * The vCPU affinity as currently edited, so the graphics tab can warn when the + * GPU worker cpuset overlaps it. Read-only view: the dialog owns the edits. + */ + @NonNull + public Map> getCurrentAffinity() { + return affinity; } /** diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/boot/VMEditBootTab.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/boot/VMEditBootTab.java index 0d3cbde55..5749d2bf8 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/boot/VMEditBootTab.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/boot/VMEditBootTab.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.vm.edit.boot; import static android.view.View.GONE; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/graphics/VMEditGraphicsTab.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/graphics/VMEditGraphicsTab.java index 3b8d9e6ee..57ee908b8 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/graphics/VMEditGraphicsTab.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/graphics/VMEditGraphicsTab.java @@ -1,58 +1,103 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.vm.edit.graphics; import static android.view.View.GONE; import static android.view.View.VISIBLE; import static java.lang.Integer.parseInt; import static cn.classfun.droidvm.lib.store.enums.Enums.optEnum; -import static cn.classfun.droidvm.lib.store.vm.DisplayBackend.SIMPLEFB; import static cn.classfun.droidvm.lib.store.vm.GpuBackend.GPU_GFXSTREAM; -import static cn.classfun.droidvm.lib.utils.StringUtils.generateRandomPassword; import static cn.classfun.droidvm.lib.utils.StringUtils.getEditText; import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; -import com.google.android.material.button.MaterialButton; import com.google.android.material.textfield.TextInputEditText; import cn.classfun.droidvm.R; +import cn.classfun.droidvm.lib.natives.VulkanBlitProbe; import cn.classfun.droidvm.lib.store.vm.VMBackend; import cn.classfun.droidvm.lib.store.vm.VMConfig; import cn.classfun.droidvm.lib.store.vm.VMStore; -import cn.classfun.droidvm.lib.store.vm.DisplayBackend; +import cn.classfun.droidvm.lib.store.vm.DisplayExporter; +import cn.classfun.droidvm.lib.store.vm.VMScreenConfig; +import cn.classfun.droidvm.lib.store.vm.VpuConfig; import cn.classfun.droidvm.lib.store.vm.GpuApi; import cn.classfun.droidvm.lib.store.vm.GpuBackend; +import cn.classfun.droidvm.lib.store.vm.GpuBlitProvider; +import cn.classfun.droidvm.lib.store.vm.GpuMode; +import cn.classfun.droidvm.lib.store.vm.GpuProvider; +import cn.classfun.droidvm.lib.store.vm.ProtectedVM; +import cn.classfun.droidvm.lib.store.vm.CpuPlacementPlan; +import cn.classfun.droidvm.lib.utils.CpuUtils; import cn.classfun.droidvm.ui.vm.edit.VMEditActivity; import cn.classfun.droidvm.ui.vm.edit.base.VMEditBaseTab; +import cn.classfun.droidvm.ui.vm.edit.base.VMEditTab; +import cn.classfun.droidvm.ui.vm.edit.basic.VMEditBasicTab; import cn.classfun.droidvm.ui.widgets.row.ChooseRowWidget; import cn.classfun.droidvm.ui.widgets.row.SwitchRowWidget; +import cn.classfun.droidvm.ui.widgets.row.TextRowWidget; +import cn.classfun.droidvm.ui.widgets.tools.CpuCorePickerDialog; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; public final class VMEditGraphicsTab extends VMEditBaseTab { - private static final int VNC_PASSWORD_LENGTH = 8; + /** Set while loadConfig() is applying stored values, so enforcement stays silent. */ + private boolean loadingConfig = false; + /** The config this tab was loaded with; the protection mode is read back out of it. */ + @Nullable + private VMConfig loadedConfig; private View gpuOptions; - private View displayOptions; - private View displayDpiOptions; - private View vncOptions; - private View vncPasswordOptions; - private SwitchRowWidget swGpuEnabled; - private SwitchRowWidget swVncEnabled; - private SwitchRowWidget swVncPasswordAuth; - private SwitchRowWidget swDisplayEnabled; - private SwitchRowWidget swNativeDisplayEnabled; + private View rendererSection; + private View vramSettings; + private View tilGpuDrm2KgslPoolMb; + private View tilGpuHostPoolMb; + private View tilGpuVenusPoolMb; + private TextInputEditText etGpuDrm2KgslPoolMb; + private TextInputEditText etGpuVenusPoolMb; + private View dynamicVramOptions; + private View tilGpuGuestPoolMb; + private SwitchRowWidget swGpuUdmabuf; + private SwitchRowWidget swGpuRtPrio; + private SwitchRowWidget swGpuDynamicVram; + private TextInputEditText etGpuHostPoolMb; + private TextInputEditText etGpuVramQuotaMb; + private TextInputEditText etGpuVramFolioThresholdKb; + private TextInputEditText etGpuGuestPoolMb; + private View dynamicVramHostOptions; + private View dynamicVramGuestOptions; + private TextInputEditText etGpuGuestPreallocMb; + private TextInputEditText etGpuGuestStepMb; + private TextInputEditText etGpuGuestMaxGrants; + private TextInputEditText etGpuPoolBlobMaxKb; private ChooseRowWidget chooseGpuBackend; - private ChooseRowWidget chooseGpuApi; - private ChooseRowWidget chooseDisplayBackend; - private TextInputEditText etDisplayWidth; - private TextInputEditText etDisplayHeight; - private TextInputEditText etDisplayRefreshRate; - private TextInputEditText etDisplayDpiH; - private TextInputEditText etDisplayDpiV; - private TextInputEditText etVncHost; - private TextInputEditText etVncPort; - private TextInputEditText etVncPassword; - private MaterialButton btnVncPasswordClear; - private MaterialButton btnVncPasswordGenerate; + private ChooseRowWidget chooseGpuMode; + private ChooseRowWidget chooseGpuProvider; + private ChooseRowWidget chooseDisplayBlitProvider; + /** One block per screen, in VMScreenConfig.IDS order. */ + private ScreenBindingRow screenGpu0; + private ScreenBindingRow screenFb; + private SwitchRowWidget swGpuCgroup; + private SwitchRowWidget swVpuEnabled; + private View vpuOptions; + private View mediaGuestPoolOptions; + private TextInputEditText etMediaHostPoolMb; + private TextInputEditText etMediaGuestPoolMb; + private View gpuCgroupOptions; + private TextInputEditText etGpuCgroupPath; + private TextRowWidget rowGpuCgroupCpus; + + /** Host cores in the GPU worker cpuset, as a CPUSET string. */ + private String gpuCgroupCpus = ""; + /** Host cores as reported by sysfs; read once, drives the cpuset picker. */ + private List hostCores = List.of(); public VMEditGraphicsTab(VMEditActivity parent, View view) { super(parent, view); @@ -60,89 +105,328 @@ public VMEditGraphicsTab(VMEditActivity parent, View view) { @Override public void initView() { - swGpuEnabled = view.findViewById(R.id.sw_gpu_enabled); chooseGpuBackend = view.findViewById(R.id.choose_gpu_backend); - chooseGpuApi = view.findViewById(R.id.choose_gpu_api); + chooseGpuMode = view.findViewById(R.id.choose_gpu_mode); + chooseGpuProvider = view.findViewById(R.id.choose_gpu_provider); gpuOptions = view.findViewById(R.id.gpu_options); - swDisplayEnabled = view.findViewById(R.id.sw_display_enabled); - swNativeDisplayEnabled = view.findViewById(R.id.sw_native_display_enabled); - chooseDisplayBackend = view.findViewById(R.id.choose_display_backend); - displayOptions = view.findViewById(R.id.display_options); - etDisplayWidth = view.findViewById(R.id.et_display_width); - etDisplayHeight = view.findViewById(R.id.et_display_height); - etDisplayRefreshRate = view.findViewById(R.id.et_display_refresh_rate); - etDisplayDpiH = view.findViewById(R.id.et_display_dpi_h); - etDisplayDpiV = view.findViewById(R.id.et_display_dpi_v); - displayDpiOptions = view.findViewById(R.id.display_dpi_options); - swVncEnabled = view.findViewById(R.id.sw_vnc_enabled); - swVncPasswordAuth = view.findViewById(R.id.sw_vnc_password_auth); - vncOptions = view.findViewById(R.id.vnc_options); - vncPasswordOptions = view.findViewById(R.id.vnc_password_options); - etVncHost = view.findViewById(R.id.et_vnc_host); - etVncPort = view.findViewById(R.id.et_vnc_port); - etVncPassword = view.findViewById(R.id.et_vnc_password); - btnVncPasswordClear = view.findViewById(R.id.btn_vnc_password_clear); - btnVncPasswordGenerate = view.findViewById(R.id.btn_vnc_password_generate); + rendererSection = view.findViewById(R.id.renderer_section); + swGpuUdmabuf = view.findViewById(R.id.sw_gpu_udmabuf); + swGpuRtPrio = view.findViewById(R.id.sw_gpu_rt_prio); + vramSettings = view.findViewById(R.id.vram_settings); + swGpuDynamicVram = view.findViewById(R.id.sw_gpu_dynamic_vram); + dynamicVramOptions = view.findViewById(R.id.dynamic_vram_options); + tilGpuGuestPoolMb = view.findViewById(R.id.til_gpu_guest_pool_mb); + tilGpuDrm2KgslPoolMb = view.findViewById(R.id.til_gpu_drm2kgsl_pool_mb); + tilGpuHostPoolMb = view.findViewById(R.id.til_gpu_host_pool_mb); + tilGpuVenusPoolMb = view.findViewById(R.id.til_gpu_venus_pool_mb); + etGpuDrm2KgslPoolMb = view.findViewById(R.id.et_gpu_drm2kgsl_pool_mb); + etGpuHostPoolMb = view.findViewById(R.id.et_gpu_host_pool_mb); + etGpuVenusPoolMb = view.findViewById(R.id.et_gpu_venus_pool_mb); + etGpuVramQuotaMb = view.findViewById(R.id.et_gpu_vram_quota_mb); + etGpuVramFolioThresholdKb = + view.findViewById(R.id.et_gpu_vram_folio_threshold_kb); + etGpuGuestPoolMb = view.findViewById(R.id.et_gpu_guest_pool_mb); + dynamicVramHostOptions = view.findViewById(R.id.dynamic_vram_host_options); + dynamicVramGuestOptions = view.findViewById(R.id.dynamic_vram_guest_options); + etGpuGuestPreallocMb = view.findViewById(R.id.et_gpu_guest_prealloc_mb); + etGpuGuestStepMb = view.findViewById(R.id.et_gpu_guest_step_mb); + etGpuGuestMaxGrants = view.findViewById(R.id.et_gpu_guest_max_grants); + etGpuPoolBlobMaxKb = view.findViewById(R.id.et_gpu_pool_blob_max_kb); + chooseDisplayBlitProvider = view.findViewById(R.id.choose_display_blit_provider); + // A new VM comes up with the simplefb screen on and no virtio-gpu device at all until the + // user asks for one. This is where a new screen's exporter default lives -- the stored + // config's fallback is a separate question and stays at NONE, see + // VMScreenConfig.getExporter -- and it is NATIVE rather than the VNC it used to be: the + // viewer for that one is this app, already installed, so it is the only exporter whose + // first boot can be looked at without setting something up first. The gpu-0 row's default + // is inert while its screen is off (save() writes NONE for a screen that is off) and is + // there for the moment the user turns the device on. + screenGpu0 = new ScreenBindingRow(VMScreenConfig.ID_GPU0, + view.findViewById(R.id.screen_gpu0_block), + view.findViewById(R.id.sw_screen_gpu0_enabled), + false, DisplayExporter.NATIVE); + screenFb = new ScreenBindingRow(VMScreenConfig.ID_SIMPLEFB, + view.findViewById(R.id.screen_fb_block), + view.findViewById(R.id.sw_screen_fb_enabled), + true, DisplayExporter.NATIVE); + swGpuCgroup = view.findViewById(R.id.sw_gpu_cgroup); + swVpuEnabled = view.findViewById(R.id.sw_vpu_enabled); + vpuOptions = view.findViewById(R.id.vpu_options); + mediaGuestPoolOptions = view.findViewById(R.id.media_guest_pool_options); + etMediaHostPoolMb = view.findViewById(R.id.et_media_host_pool_mb); + etMediaGuestPoolMb = view.findViewById(R.id.et_media_guest_pool_mb); + gpuCgroupOptions = view.findViewById(R.id.gpu_cgroup_options); + etGpuCgroupPath = view.findViewById(R.id.et_gpu_cgroup_path); + rowGpuCgroupCpus = view.findViewById(R.id.row_gpu_cgroup_cpus); } @Override public void initValue() { - swGpuEnabled.setOnCheckedChangeListener(this::updateGpuVisibility); - swDisplayEnabled.setOnCheckedChangeListener(this::updateDisplayVisibility); - swVncEnabled.setOnCheckedChangeListener(this::updateVncVisibility); - chooseGpuBackend.configure(GpuBackend.class, GPU_GFXSTREAM); - chooseGpuApi.configure(GpuApi.class, GpuApi.OPENGLES); - chooseDisplayBackend.configure(DisplayBackend.class, SIMPLEFB); - chooseDisplayBackend.setOnValueChangedListener((o, n) -> { - if (n == DisplayBackend.VIRTIO_GPU && !swGpuEnabled.isChecked()) { - chooseDisplayBackend.setSelectedItem(SIMPLEFB); - return; + screenGpu0.init(this::updateDisplayVisibility); + screenFb.init(this::updateDisplayVisibility); + // PanVK (Mali) is listed but not wired yet: toast + revert to the previous choice. + // Acceleration decides which host drivers make sense and which memory knobs exist, + // so it drives both of the rows under it. + chooseGpuMode.setOnValueChangedListener((o, n) -> { + // Vulkan on virglrenderer is venus (guest Vulkan proxied to the host over virtio-gpu); + // its provider row is the host ICD, same as gfxstream's, so this is just a normal mode + // change like NATIVE/OPENGL. + updateGpuProviderOptions(); + updateVramAllocVisibility(); + }); + // PanVK (Mali) is listed but not wired yet: toast + revert to the previous choice. + chooseGpuProvider.setOnValueChangedListener((o, n) -> { + if (n == GpuProvider.VK_PANVK) { + Toast.makeText(parent, R.string.create_vm_gpu_api_not_implemented, + Toast.LENGTH_SHORT).show(); + chooseGpuProvider.setSelectedItem( + o instanceof GpuProvider ? (GpuProvider) o : GpuProvider.VK_TURNIP); + } + }); + // The GPU API set depends on the backend: gfxstream picks a host Vulkan driver, + // virgl/2d picks a guest GL/Vulkan translation API or the drm2kgsl native context. + chooseGpuBackend.setOnValueChangedListener((o, n) -> { + updateGpuModeOptions(); + updateGpuProviderOptions(); + updateVramAllocVisibility(); + }); + chooseGpuBackend.configure(GpuBackend.class, GpuBackend.GPU_VIRGLRENDERER); + // GPU-blit provider for the native display's scanout composite -- a peer set of Vulkan + // drivers plus an Off (CPU copy) escape. TURNIP/SYSTEM/OFF are wired (SYSTEM points the + // bridge at the SoC's vendor Vulkan and degrades to the CPU copy if it lacks the extensions); + // only PANVK is not built yet, so it toasts + reverts like the render provider's PanVK. + chooseDisplayBlitProvider.configure(GpuBlitProvider.class, GpuBlitProvider.TURNIP); + chooseDisplayBlitProvider.setOnValueChangedListener((o, n) -> { + if (n == GpuBlitProvider.PANVK) { + Toast.makeText(parent, R.string.create_vm_gpu_api_not_implemented, + Toast.LENGTH_SHORT).show(); + chooseDisplayBlitProvider.setSelectedItem( + o instanceof GpuBlitProvider ? (GpuBlitProvider) o : GpuBlitProvider.TURNIP); + } else if (n == GpuBlitProvider.SYSTEM) { + // The system driver is a peer, gated by the same rule as the others: it is usable + // only if it exposes the blit's extensions. Probe the real driver and, if it is + // short, say which -- the choice is still allowed (crosvm then degrades to a CPU + // copy), the user just gets told instead of quietly getting the fallback. + warnIfSystemBlitIncapable(); } - updateDisplayDpiVisibility(); }); - btnVncPasswordClear.setOnClickListener(v -> etVncPassword.setText("")); - btnVncPasswordGenerate.setOnClickListener(v -> - etVncPassword.setText(generateRandomPassword(VNC_PASSWORD_LENGTH))); - swVncPasswordAuth.setOnCheckedChangeListener((b, checked) -> - updateVncPasswordVisibility()); - updateGpuVisibility(); + swGpuUdmabuf.setOnCheckedChangeListener(this::updateVramAllocVisibility); + swGpuDynamicVram.setOnCheckedChangeListener(this::updateVramAllocVisibility); + // The backend chooser's configure() above does not fire its listener, so seed the two + // rows under it before the first paint; loadConfig() then restores over the top. + updateGpuModeOptions(); + updateGpuProviderOptions(); + updateVramAllocVisibility(); updateDisplayVisibility(); - updateDisplayDpiVisibility(); - updateVncVisibility(); - updateVncPasswordVisibility(); + updateVramAllocVisibility(); + initGpuCgroup(); + } + + private void initGpuCgroup() { + hostCores = CpuUtils.getCores(); + etGpuCgroupPath.setText(CpuPlacementPlan.DEFAULT_GPU_CGROUP_PATH); + swGpuCgroup.setOnCheckedChangeListener(() -> { + // Default to the top (usually prime) core, the one worth keeping vCPUs off. + if (swGpuCgroup.isChecked() && gpuCgroupCpus.trim().isEmpty() + && !hostCores.isEmpty()) + setGpuCgroupCpus(String.valueOf(hostCores.get(hostCores.size() - 1).index)); + updateGpuCgroupVisibility(); + }); + rowGpuCgroupCpus.setOnClickListener(v -> showGpuCpusPicker()); + swVpuEnabled.setOnCheckedChangeListener(() -> updateVpuVisibility()); + // RT is only offered on top of a cpuset with cores in it. The switch is inside the + // cpuset block, so it is already hidden when the block is off; this catches the one + // reachable gap -- the block on, but every core cleared in the picker. + // + // Not while loading: loadConfig() restores this switch before the cpuset rows below it, + // so the guard would see an empty core list for every VM and clear a legitimately stored + // RT setting. The updateGpuCgroupVisibility() at the end of the load enforces the same + // rule once both halves are in place. + swGpuRtPrio.setOnCheckedChangeListener(() -> { + if (loadingConfig) return; + if (swGpuRtPrio.isChecked() && !hasGpuCgroupCpus()) { + swGpuRtPrio.setChecked(false); + showHint(parent.getString( + R.string.create_vm_error_gpu_rt_prio_needs_cpuset)); + } + }); + // Unconditionally, so the row always carries a value label ("none selected" + // when empty) rather than a bare title with a blank right-hand side. + setGpuCgroupCpus(gpuCgroupCpus); + updateGpuCgroupVisibility(); + } + + private void updateGpuCgroupVisibility() { + boolean enabled = swGpuCgroup.isChecked(); + gpuCgroupOptions.setVisibility(enabled ? VISIBLE : GONE); + // RT lives inside the block, so turning the cpuset off hides it. Clear it rather than + // leaving a checked-but-invisible switch: without the cpuset, SCHED_FIFO 97 GPU threads + // are free to take every host core above everything else on the phone. Same for a + // stored config that has RT on with the block on but no cores named -- the cpuset would + // never be set up (the daemon skips an empty cpus list), so the confinement is absent. + if (!enabled || !hasGpuCgroupCpus()) swGpuRtPrio.setChecked(false); + } + + /** Whether the cpuset actually names cores; RT is refused without them. */ + private boolean hasGpuCgroupCpus() { + return !gpuCgroupCpus.trim().isEmpty(); + } + + private void showGpuCpusPicker() { + CpuCorePickerDialog.show( + parent, R.string.create_vm_gpu_cgroup_cpus, gpuCgroupCpus, + picked -> { + setGpuCgroupCpus(picked); + // Clearing the last core takes RT down with it, for the same reason the switch + // refuses to come up without cores. + if (!hasGpuCgroupCpus() && swGpuRtPrio.isChecked()) { + swGpuRtPrio.setChecked(false); + showHint(parent.getString( + R.string.create_vm_error_gpu_rt_prio_needs_cpuset)); + } + warnOnGpuCgroupOverlap(); + }); + } + + private void setGpuCgroupCpus(@NonNull String cpuSet) { + gpuCgroupCpus = cpuSet; + var compact = CpuUtils.compactRanges(cpuSet); + rowGpuCgroupCpus.setValue(compact.isEmpty() + ? parent.getString(R.string.create_vm_gpu_cgroup_cpus_none) + : parent.getString(R.string.create_vm_gpu_cgroup_cpus_summary_fmt, compact)); + } + + /** + * Sharing a core between vCPUs and the GPU worker is allowed -- on a host with + * few big cores it is often the only workable split -- but it defeats the point + * of the cpuset, so say so instead of silently accepting it. The affinity lives + * in the basic tab, so it is read from there rather than held here. + */ + private void warnOnGpuCgroupOverlap() { + if (!swGpuCgroup.isChecked()) return; + Map> affinity = Map.of(); + try { + var basic = (VMEditBasicTab) parent.getTab(VMEditTab.TAB_BASIC); + affinity = basic.getCurrentAffinity(); + } catch (Exception ignored) { + } + if (affinity.isEmpty()) return; + var shared = CpuPlacementPlan.findHostOverlaps(affinity, gpuCgroupCpus); + if (shared.isEmpty()) return; + var sb = new StringBuilder(); + for (var core : shared) { + if (sb.length() > 0) sb.append(','); + sb.append(core); + } + showHint(parent.getString( + R.string.create_vm_cpu_affinity_host_overlap, sb.toString())); } @Override public void loadConfig(@NonNull VMConfig config) { + loadedConfig = config; + loadingConfig = true; + try { + loadConfigLocked(config); + } finally { + loadingConfig = false; + } + } + + private void loadConfigLocked(@NonNull VMConfig config) { var item = config.item; - swGpuEnabled.setChecked(item.optBoolean("gpu_enabled", false)); - swDisplayEnabled.setChecked(item.optBoolean("display_enabled", false)); - swNativeDisplayEnabled.setChecked(item.optBoolean("native_display_enabled", false)); - etDisplayWidth.setText(String.valueOf(item.optLong("display_width", 1280))); - etDisplayHeight.setText(String.valueOf(item.optLong("display_height", 720))); - etDisplayRefreshRate.setText(String.valueOf(item.optLong("display_refresh_rate", 60))); - etDisplayDpiH.setText(String.valueOf(item.optLong("display_dpi_h", 160))); - etDisplayDpiV.setText(String.valueOf(item.optLong("display_dpi_v", 160))); - swVncEnabled.setChecked(item.optBoolean("vnc_enabled", false)); - swVncPasswordAuth.setChecked(item.optBoolean("vnc_password_auth", false)); - etVncHost.setText(item.optString("vnc_host", "")); - var vncPort = item.optLong("vnc_port", -1); - etVncPort.setText(vncPort > 0 ? String.valueOf(vncPort) : ""); - etVncPassword.setText(item.optString("vnc_password", "")); - var gpuApi = optEnum(item, "gpu_api", GpuApi.NONE); + swGpuUdmabuf.setChecked(item.optBoolean("gpu_udmabuf", true)); + // SCHED_FIFO on the GPU worker; gpu_rt_prio holds the level as a string ("" by default), + // and "" means "do not set RT at all". Off (the default) -> normal scheduling, which avoids + // the gfxstream render-thread priority inversion that starves the present path. The cpuset + // rows are restored at the end of this method; updateGpuCgroupVisibility() there clears + // this again if the config carries RT without a cpuset to confine it. + swGpuRtPrio.setChecked(!item.optString("gpu_rt_prio", "").isEmpty()); + // The two host pools hold very different things, so they are sized very differently. + // + // The DRM route's host pool holds only the per-context msm shmem rings: 16 KiB each, + // 192 KiB measured across a desktop plus Minecraft, on top of the 2 MiB the RM base guard + // takes at the head of the pool -- so the need is single-digit MB and 64 MiB is a wide + // margin, matching what the gfxstream pool below reserves for the same kind of traffic. + etGpuDrm2KgslPoolMb.setText(String.valueOf(item.optLong("gpu_drm2kgsl_pool_mb", 64))); + // gfxstream's holds its ASG rings, one per guest context, at 1036 KiB each -- a desktop + // plus Minecraft was 16 of them, 18.2 MiB including the guard. 64 MiB is room for ~61 + // contexts, and this is mlocked physical memory, so the spare is not free. + // + // It is a floor rather than a cap: with guest-alloc off, host-visible VkDeviceMemory + // comes from here too and whatever does not fit falls back to runtime SHARE, which is + // what the dynamic-VRAM switch below governs. So a pool that is too small costs speed, + // not correctness -- 64 MiB leaves ~46 MiB of that fast path once the rings are in. + etGpuHostPoolMb.setText(String.valueOf(item.optLong("gpu_host_pool_mb", 64))); + // venus's holds its per-instance transport shmems (ring 128K + CS pool >=8M + reply pool + // >=1M); vkr sub-allocates them here and the guest maps pool-relative with no runtime SHARE. + // Bigger avoids a memfd fallback under KDE's several Vulkan instances; but on a small- + // reservoir device (e.g. the 8gen3's 5.5 GB cap) it competes with guest RAM + the guest + // pool, so it is exposed here to tune per device rather than hard-wired to the daemon default. + etGpuVenusPoolMb.setText(String.valueOf(item.optLong("gpu_venus_pool_mb", 256))); + // A quota, not an allocation: crosvm meters host-visible memory against this rather + // than reserving any. Only read when guest-alloc is off. + etGpuVramQuotaMb.setText(String.valueOf(item.optLong("gpu_vram_quota_mb", 2048))); + etGpuVramFolioThresholdKb.setText(String.valueOf( + item.optLong("gpu_vram_folio_threshold_kb", 1024))); + long guestPool = item.optLong("gpu_guest_pool_mb", 1024); + etGpuGuestPoolMb.setText(String.valueOf(guestPool)); + etGpuGuestPreallocMb.setText(String.valueOf( + item.optLong("gpu_guest_prealloc_mb", guestPool))); + etGpuGuestStepMb.setText(String.valueOf(item.optLong("gpu_guest_step_mb", 0))); + etGpuGuestMaxGrants.setText(String.valueOf(item.optLong("gpu_guest_max_grants", 0))); + // Defaults to on: before it had a switch, a vram limit was always handed to crosvm. Over + // a guest pool "on" just exposes the three growth fields above -- their own defaults + // (prealloc = pool, step 0) still describe a fully pre-shared pool, so an older config + // loads with the same behaviour it had. + swGpuDynamicVram.setChecked(item.optBoolean("gpu_dynamic_vram", false)); + etGpuPoolBlobMaxKb.setText(String.valueOf(item.optLong("gpu_pool_blob_max_kb", 4096))); + screenGpu0.load(item); + screenFb.load(item); var gpuBackend = optEnum(item, "gpu_backend", GpuBackend.NONE); - var displayBackend = optEnum(item, "display_backend", DisplayBackend.NONE); - if (gpuApi != GpuApi.NONE) - chooseGpuApi.setSelectedItem(gpuApi); + // Configs written before acceleration and host driver were separate rows carry a single + // gpu_api whose meaning depended on the renderer. Derive both from it when the new keys + // are absent; gpu_api is left in place, so an older build still reads the VM. + var legacyApi = optEnum(item, "gpu_api", GpuApi.NONE); + var gpuMode = optEnum(item, "gpu_mode", GpuMode.NONE); + var gpuProvider = optEnum(item, "gpu_provider", GpuProvider.NONE); + if (gpuMode == GpuMode.NONE) gpuMode = GpuMode.fromLegacyApi(legacyApi); + if (gpuProvider == GpuProvider.NONE) gpuProvider = GpuProvider.fromLegacyApi(legacyApi); + // Backend first: its listener rebuilds the option sets below it (and their defaults). if (gpuBackend != GpuBackend.NONE) chooseGpuBackend.setSelectedItem(gpuBackend); - if (displayBackend != DisplayBackend.NONE) - chooseDisplayBackend.setSelectedItem(displayBackend); - updateGpuVisibility(); + // Restore each saved choice only if the backend still offers it; otherwise keep the + // default updateGpuModeOptions()/updateGpuProviderOptions() just picked. + boolean gfx = chooseGpuBackend.getSelectedItem() == GPU_GFXSTREAM; + // gfxstream only offers VULKAN; virglrenderer offers OPENGL/NATIVE/VULKAN(venus). Restore + // any mode the current backend actually lists, or the picker keeps the default and a saved + // venus VM silently reloads as something else. + boolean modeOk = gfx ? (gpuMode == GpuMode.VULKAN) + : (gpuMode == GpuMode.OPENGL || gpuMode == GpuMode.NATIVE || gpuMode == GpuMode.VULKAN); + if (gpuMode != GpuMode.NONE && modeOk) + chooseGpuMode.setSelectedItem(gpuMode); + updateGpuProviderOptions(); + // The VK_* providers belong to VULKAN mode on either renderer (gfxstream, or venus on + // virglrenderer); everything else is virglrenderer-only. Restore only what the row lists. + // (Read the mode picker only under virglrenderer: 2D leaves it unset.) + boolean vkMode = gfx + || (chooseGpuBackend.getSelectedItem() == GpuBackend.GPU_VIRGLRENDERER + && chooseGpuMode.getSelectedItem() == GpuMode.VULKAN); + if (gpuProvider != GpuProvider.NONE && vkMode == isVulkanProvider(gpuProvider)) + chooseGpuProvider.setSelectedItem(gpuProvider); + chooseDisplayBlitProvider.setSelectedItem( + optEnum(item, "display_blit_provider", GpuBlitProvider.TURNIP)); updateDisplayVisibility(); - updateDisplayDpiVisibility(); - updateVncVisibility(); - updateVncPasswordVisibility(); + updateVramAllocVisibility(); + swGpuCgroup.setChecked(item.optBoolean(CpuPlacementPlan.KEY_GPU_CGROUP, false)); + swVpuEnabled.setChecked(VpuConfig.isEnabled(item)); + etMediaHostPoolMb.setText(String.valueOf(VpuConfig.getHostPoolMb(item))); + etMediaGuestPoolMb.setText(String.valueOf(VpuConfig.getGuestPoolMb(item))); + updateVpuVisibility(); + etGpuCgroupPath.setText(item.optString(CpuPlacementPlan.KEY_GPU_CGROUP_PATH, + CpuPlacementPlan.DEFAULT_GPU_CGROUP_PATH)); + setGpuCgroupCpus(item.optString(CpuPlacementPlan.KEY_GPU_CGROUP_CPUS, "")); + updateGpuCgroupVisibility(); } @SuppressWarnings("BooleanMethodIsAlwaysInverted") @@ -166,92 +450,482 @@ private boolean checkInputField( @Override public boolean validateInput(@NonNull VMStore store) { - if (!checkInputField(etDisplayWidth, false, 320, 8192)) return false; - if (!checkInputField(etDisplayHeight, false, 320, 8192)) return false; - if (!checkInputField(etDisplayRefreshRate, false, 1, 400)) return false; - if (!checkInputField(etDisplayDpiH, false, 100, 800)) return false; - if (!checkInputField(etDisplayDpiV, false, 100, 800)) return false; - if (!checkInputField(etVncPort, true, 1024, 65535)) return false; - if (parent.get("backend", VMBackend.DEFAULT) != VMBackend.CROSVM && swNativeDisplayEnabled.isChecked()) - return showValidateFailed(R.string.create_vm_error_native_display_only_crosvm); - if (swNativeDisplayEnabled.isChecked() && swVncEnabled.isChecked()) - return showValidateFailed(R.string.create_vm_error_native_display_vnc_cannot_enabled); + if (!validateScreenGeometry()) return false; + if (!checkInputField(etGpuHostPoolMb, false, 0, 65536)) return false; + if (!checkInputField(etGpuVramQuotaMb, false, 0, 65536)) return false; + if (!checkInputField(etGpuVramFolioThresholdKb, false, 0, 1048576)) return false; + if (!checkInputField(etGpuGuestPoolMb, false, 0, 65536)) return false; + if (!checkInputField(etGpuPoolBlobMaxKb, false, 0, 1048576)) return false; + if (swVpuEnabled.isChecked()) { + if (!checkInputField(etMediaHostPoolMb, false, 0, 65536)) return false; + // Checked only when it is offered: a hidden field holds whatever the config carried + // over from another protection mode, and rejecting a value nobody can see or edit + // would make the VM unsaveable for a reason the screen does not show. + if (VpuConfig.guestPoolApplies(currentProtectedVm()) + && !checkInputField(etMediaGuestPoolMb, false, 0, 65536)) return false; + } + // The growth fields are only read (and only shown) with dynamic vram on over a guest + // pool; otherwise saveConfig() writes the fully pre-shared values in their place, so a + // stale entry in a hidden field must not block the save. + if (isGuestPoolDynamic()) { + if (!checkInputField(etGpuGuestPreallocMb, false, 0, 65536)) return false; + if (!checkInputField(etGpuGuestStepMb, false, 0, 65536)) return false; + if (!checkInputField(etGpuGuestMaxGrants, false, 0, 65536)) return false; + if (!validateGuestPoolOptions()) return false; + } + if (!validateScreens()) return false; + if (swGpuCgroup.isChecked()) { + var path = getEditText(etGpuCgroupPath).trim(); + if (path.isEmpty() || !path.startsWith("/")) + return showValidateFailed(R.string.create_vm_error_gpu_cgroup_path); + if (gpuCgroupCpus.trim().isEmpty()) + return showValidateFailed(R.string.create_vm_error_gpu_cgroup_cpus_empty); + } + return true; + } + + /** + * Each screen's own geometry, checked on each screen's own rows. + * + *

    Size and rate are picked from menus, so the row itself holds their bounds -- including the + * one that differs by device: a virtio-gpu mode's refresh rate is what the guest is told to + * drive, while simplefb's poll rate is how often the host looks at a block of memory (linear + * host cost, no guest involvement), so it is bounded by what the bridge accepts rather than by + * what a panel could show. The DPI is still typed, and is still checked here.

    + */ + private boolean validateScreenGeometry() { + for (var row : new ScreenBindingRow[]{screenGpu0, screenFb}) { + if (!row.validateGeometry()) return false; + if (row.isGpuScreen()) { + if (!checkInputField(row.dpiHField(), false, 100, 800)) return false; + if (!checkInputField(row.dpiVField(), false, 100, 800)) return false; + } + } + return true; + } + + private boolean validateGuestPoolOptions() { + int guestPool = parseInt(getEditText(etGpuGuestPoolMb)); + int prealloc = parseInt(getEditText(etGpuGuestPreallocMb)); + int step = parseInt(getEditText(etGpuGuestStepMb)); + if (prealloc > guestPool) { + etGpuGuestPreallocMb.setError( + parent.getString(R.string.create_vm_error_gpu_guest_prealloc_exceeds_pool)); + return false; + } + if (step > 0 && prealloc >= guestPool) { + etGpuGuestPreallocMb.setError( + parent.getString(R.string.create_vm_error_gpu_guest_dynamic_requires_room)); + return false; + } + if (step > 0 && (step < 2 || (step & (step - 1)) != 0)) { + etGpuGuestStepMb.setError( + parent.getString(R.string.create_vm_error_gpu_guest_step_invalid)); + return false; + } return true; } + private boolean usesGuestPool() { + boolean gfxstream = chooseGpuBackend.getSelectedItem() == GPU_GFXSTREAM; + // Read the mode only under virglrenderer: 2D clears the mode picker and reading it would + // throw "Items not set". drm2kgsl only exists as virglrenderer's NATIVE mode anyway. + boolean drm2kgsl = chooseGpuBackend.getSelectedItem() == GpuBackend.GPU_VIRGLRENDERER + && chooseGpuMode.getSelectedItem() == GpuMode.NATIVE; + // Venus (virglrenderer + VULKAN) allocates its BOs from the same guest pool. + boolean venus = chooseGpuBackend.getSelectedItem() == GpuBackend.GPU_VIRGLRENDERER + && chooseGpuMode.getSelectedItem() == GpuMode.VULKAN; + return drm2kgsl || venus || (gfxstream && swGpuUdmabuf.isChecked()); + } + + /** Whether the VRAM block applies at all: gfxstream (either alloc path), drm2kgsl or venus. */ + private boolean usesVramSettings() { + boolean gfxstream = chooseGpuBackend.getSelectedItem() == GPU_GFXSTREAM; + return gfxstream || usesGuestPool(); + } + + /** + * Whether the guest pool grows at runtime: guest-alloc with the dynamic-vram switch on. Off + * means the whole pool is SHARE'd at boot (prealloc = pool, step 0, no grants), which is + * what saveConfig() writes and what crosvm's own defaults produce for the three keys. + */ + private boolean isGuestPoolDynamic() { + return usesGuestPool() && swGpuDynamicVram.isChecked(); + } + + @Override + public void onTabShown() { + // The protection mode is chosen on the basic tab, and it decides whether the guest-alloc + // pool is offered at all. Nothing tells this tab when that changes, so ask on the way in. + updateVramAllocVisibility(); + // Same reason: the media guest pool is only offered to a VM whose memory the host cannot + // read, which is also decided over there. + updateVpuVisibility(); + } + + /** + * Whether the host can reach this VM's RAM without being handed it. + * + * The guest-alloc pool is a region the host is given access to so that buffers the guest + * allocates are reachable at all -- in an ordinary protected VM they otherwise are not. An + * unprotected VM never had that problem, and a pseudo-unprotected one has its whole RAM + * shared back to the host before the payload runs, so in both the pool is a slice of memory + * carved out of the guest to solve something that is not happening. virtio-gpu with no pool + * to find falls back to allocating from system RAM, which is the stock behaviour and works + * here for exactly the same reason. + * + * Live from the basic tab so a just-changed selection counts, then the stored config, then + * the backend default -- the same order {@code VMEditBootTab.isProtectedVm} reads it in. + */ + private boolean hostVisibleRam() { + var pvm = currentProtectedVm(); + return pvm == ProtectedVM.PROTECTED_NORMAL || pvm == ProtectedVM.PSEUDO_UNPROTECTED; + } + + /** + * The protection mode this VM will start with: live from the basic tab so a just-changed + * selection counts, then the stored config, then the backend default. + */ + @NonNull + private ProtectedVM currentProtectedVm() { + ProtectedVM pvm = null; + try { + var basic = (VMEditBasicTab) parent.getTab(VMEditTab.TAB_BASIC); + pvm = basic.getCurrentProtectedVm(); + } catch (Exception ignored) { + } + if (pvm == null && loadedConfig != null) + pvm = optEnum(loadedConfig.item, "protected_vm", ProtectedVM.PROTECTED_WITHOUT_FIRMWARE); + return pvm == null ? ProtectedVM.PROTECTED_WITHOUT_FIRMWARE : pvm; + } + + /** + * The pool fields only exist while the device does, and the guest one only while the host + * cannot read guest memory. Hiding it is not cosmetic: with no field there is no + * {@code media_guest} node, and with no node in /reserved-memory the guest driver stays on + * its stock path of allocating from system RAM. + */ + private void updateVpuVisibility() { + boolean enabled = swVpuEnabled.isChecked(); + vpuOptions.setVisibility(enabled ? VISIBLE : GONE); + mediaGuestPoolOptions.setVisibility( + enabled && VpuConfig.guestPoolApplies(currentProtectedVm()) ? VISIBLE : GONE); + } + @Override public void saveConfig(@NonNull VMConfig config) { var item = config.item; - var gpuEnabled = swGpuEnabled.isChecked(); - var displayEnabled = swDisplayEnabled.isChecked(); - var vncEnabled = swVncEnabled.isChecked(); - item.set("gpu_enabled", gpuEnabled); - item.set("display_enabled", displayEnabled); - item.set("vnc_enabled", vncEnabled); - if (gpuEnabled) { + // One binding per screen, written straight through: the pair of VM-level booleans that + // used to stand in for it could not say which screen either one meant, and could say + // "both" -- which is the combination crosvm now refuses to start. The geometry rides + // along inside each block for the same reason -- one width could not be two -- and so + // does the virtio-gpu device's own existence, which no longer has a switch of its own. + screenGpu0.save(item); + screenFb.save(item); + // The renderer settings describe a device that is only there when the screen is: with the + // block off they are left exactly as they were, so switching the device off and back on + // finds the renderer it had rather than a default. + if (screenGpu0.isScreenEnabled()) { GpuBackend gb = chooseGpuBackend.getSelectedItem(); - GpuApi ga = chooseGpuApi.getSelectedItem(); + // 2D clears the mode/provider pickers, so reading them would throw "Items not set". + // Persist NONE for both in that case. + boolean accel = gb == GPU_GFXSTREAM || gb == GpuBackend.GPU_VIRGLRENDERER; + GpuMode gm = accel ? chooseGpuMode.getSelectedItem() : GpuMode.NONE; + GpuProvider gp = accel ? chooseGpuProvider.getSelectedItem() : GpuProvider.NONE; item.set("gpu_backend", gb); - item.set("gpu_api", ga); - } - if (displayEnabled) { - DisplayBackend displayBackend = chooseDisplayBackend.getSelectedItem(); - item.set("display_backend", displayBackend); - // Native display only applies to virtio-gpu + GPU enabled. - item.set("native_display_enabled", gpuEnabled - && displayBackend == DisplayBackend.VIRTIO_GPU - && swNativeDisplayEnabled.isChecked()); - item.set("display_width", parseInt(getEditText(etDisplayWidth))); - item.set("display_height", parseInt(getEditText(etDisplayHeight))); - item.set("display_refresh_rate", parseInt(getEditText(etDisplayRefreshRate))); - if (displayBackend != DisplayBackend.NONE && displayBackend != SIMPLEFB) { - item.set("display_dpi_h", parseInt(getEditText(etDisplayDpiH))); - item.set("display_dpi_v", parseInt(getEditText(etDisplayDpiV))); + item.set("gpu_mode", gm); + item.set("gpu_provider", gp); + // Keep gpu_api written too: the daemon and an older build still read it, and it is + // exactly recoverable from the pair. + item.set("gpu_api", toLegacyApi(gm, gp)); + item.set("gpu_udmabuf", swGpuUdmabuf.isChecked()); + // "97" only with a cpuset holding cores under it -- the editor cannot produce any + // other combination, and writing "" for the rest keeps a config that was edited with + // the cpuset turned off from carrying unconfined RT forward. + boolean rtPrio = swGpuRtPrio.isChecked() + && swGpuCgroup.isChecked() && hasGpuCgroupCpus(); + item.set("gpu_rt_prio", rtPrio ? "97" : ""); + item.set("gpu_drm2kgsl_pool_mb", parseInt(getEditText(etGpuDrm2KgslPoolMb))); + item.set("gpu_host_pool_mb", parseInt(getEditText(etGpuHostPoolMb))); + item.set("gpu_venus_pool_mb", parseInt(getEditText(etGpuVenusPoolMb))); + item.set("gpu_vram_quota_mb", parseInt(getEditText(etGpuVramQuotaMb))); + item.set("gpu_vram_folio_threshold_kb", + parseInt(getEditText(etGpuVramFolioThresholdKb))); + int guestPool = parseInt(getEditText(etGpuGuestPoolMb)); + item.set("gpu_guest_pool_mb", guestPool); + // With dynamic vram off over a guest pool the growth knobs collapse to "fully + // pre-shared": preallocate the whole pool, no step, no grants. That is also what an + // older daemon assumes when the keys are absent, so the stored config stays honest + // even though the fields keep their last typed values while hidden. + if (isGuestPoolDynamic()) { + item.set("gpu_guest_prealloc_mb", parseInt(getEditText(etGpuGuestPreallocMb))); + item.set("gpu_guest_step_mb", parseInt(getEditText(etGpuGuestStepMb))); + item.set("gpu_guest_max_grants", parseInt(getEditText(etGpuGuestMaxGrants))); + } else { + item.set("gpu_guest_prealloc_mb", guestPool); + item.set("gpu_guest_step_mb", 0); + item.set("gpu_guest_max_grants", 0); } + item.set("gpu_dynamic_vram", swGpuDynamicVram.isChecked()); + item.set("gpu_pool_blob_max_kb", parseInt(getEditText(etGpuPoolBlobMaxKb))); + } + // Which host Vulkan composites frames into the native display's Surface. Written outside + // the virtio-gpu block since the simplefb screen's GPU-copy path: it is the native sink's + // driver choice, and a simplefb-only VM loads it too (the daemon's env predicate is + // any-native-binding for the same reason). + item.set("display_blit_provider", chooseDisplayBlitProvider.getSelectedItem()); + item.set(CpuPlacementPlan.KEY_GPU_CGROUP, swGpuCgroup.isChecked()); + VpuConfig.setEnabled(item, swVpuEnabled.isChecked()); + VpuConfig.setHostPoolMb(item, parseInt(getEditText(etMediaHostPoolMb))); + // Stored even while hidden, so flipping the protection mode back does not lose it. What + // decides whether a media_guest pool is created is VpuConfig.guestPoolMbFor, not whether + // the field was on screen. + VpuConfig.setGuestPoolMb(item, parseInt(getEditText(etMediaGuestPoolMb))); + item.set(CpuPlacementPlan.KEY_GPU_CGROUP_PATH, getEditText(etGpuCgroupPath).trim()); + item.set(CpuPlacementPlan.KEY_GPU_CGROUP_CPUS, gpuCgroupCpus.trim()); + } + + // What the selected renderer can actually proxy. + // + // gfxstream is Vulkan-only here (its GLES and composer capsets are not used, and the guest + // reaches GL through zink on top of Vulkan). virglrenderer offers OpenGL (capset virgl2), + // Native (capset drm) and Vulkan (capset venus -- "Vulkan on virglrenderer" is venus by + // definition, so the mode row is where venus lives; the provider row below picks its ICD). + private void updateGpuModeOptions() { + var backend = chooseGpuBackend.getSelectedItem(); + if (backend == GPU_GFXSTREAM) { + // gfxstream proxies Vulkan and nothing else here. + chooseGpuMode.setVisibility(VISIBLE); + chooseGpuMode.setItems(GpuMode.VULKAN); + chooseGpuMode.setSelectedItem(GpuMode.VULKAN); + } else if (backend == GpuBackend.GPU_VIRGLRENDERER) { + // virglrenderer has all three capsets: OPENGL (virgl2), NATIVE (drm2kgsl), VULKAN + // (venus). Only two of them are wired here -- the GL path is listed and refused rather + // than hidden, so the menu says what virglrenderer is for and where this build stands + // in it, and a VM stored with OPENGL still shows what it is set to. + chooseGpuMode.setVisibility(VISIBLE); + chooseGpuMode.setItems(GpuMode.NATIVE, GpuMode.OPENGL, GpuMode.VULKAN); + chooseGpuMode.setDisabledItems( + parent.getString(R.string.create_vm_option_not_implemented), GpuMode.OPENGL); + // setItems already lands on the first of them (Native Context), which is what a new + // VM comes up with; a stored mode is restored by load() afterwards. This only catches + // a selection left over from a set that no longer applies. + Object cur = chooseGpuMode.getSelectedItem(); + if (cur != GpuMode.OPENGL && cur != GpuMode.NATIVE && cur != GpuMode.VULKAN) + chooseGpuMode.setSelectedItem(GpuMode.NATIVE); + } else { + // 2D has no acceleration to choose. setItems() rejects an empty list, so hide the row + // rather than clearing it; its stale items are never read for 2D (the reads below are + // all gated on the backend being gfxstream or virglrenderer). + chooseGpuMode.setVisibility(GONE); + } + } + + // Which host driver serves the proxied calls. Native has none -- the DRM backend is + // compiled into virglrenderer for the device it runs on (KGSL on Adreno). + private void updateGpuProviderOptions() { + var backend = chooseGpuBackend.getSelectedItem(); + boolean gfxstream = backend == GPU_GFXSTREAM; + boolean twoD = backend != GPU_GFXSTREAM && backend != GpuBackend.GPU_VIRGLRENDERER; + if (twoD) { + // 2D has no host driver. setItems() rejects an empty list, so just hide the row (its + // stale items are never read for 2D). Bail before the mode read below, which would be + // reading the hidden mode picker. + chooseGpuProvider.setVisibility(GONE); + return; } - if (vncEnabled) { - var passwordAuth = swVncPasswordAuth.isChecked(); - item.set("vnc_password_auth", passwordAuth); - if (passwordAuth) - item.set("vnc_password", getEditText(etVncPassword)); - item.set("vnc_host", getEditText(etVncHost)); - var vncPortStr = getEditText(etVncPort); - item.set("vnc_port", vncPortStr.isEmpty() ? -1 : parseInt(vncPortStr)); + GpuMode mode = chooseGpuMode.getSelectedItem(); + if (gfxstream || mode == GpuMode.VULKAN) { + // Host Vulkan ICD. gfxstream is always in this mode; on virglrenderer it is venus. + // Both dlopen whatever ANDROID_EMU_VK_LOADER_PATH names (or the system loader when it + // is unset), so the choice is identical: bundled turnip, PanVK, or the SoC's stock HAL. + chooseGpuProvider.setItems( + GpuProvider.VK_TURNIP, GpuProvider.VK_PANVK, GpuProvider.VK_SYSTEM); + if (!isVulkanProvider(chooseGpuProvider.getSelectedItem())) + chooseGpuProvider.setSelectedItem(GpuProvider.VK_TURNIP); + } else if (mode == GpuMode.OPENGL) { + chooseGpuProvider.setItems(GpuProvider.EGL, GpuProvider.GLES); + if (isVulkanProvider(chooseGpuProvider.getSelectedItem()) + || chooseGpuProvider.getSelectedItem() == GpuProvider.NONE) + chooseGpuProvider.setSelectedItem(GpuProvider.GLES); + } else if (mode == GpuMode.NATIVE) { + // One backend today, but the row stays: it is what makes the config say WHICH DRM + // driver the host answers with, and it is the key the launcher branches on. + chooseGpuProvider.setItems(GpuProvider.DRM2KGSL); + chooseGpuProvider.setSelectedItem(GpuProvider.DRM2KGSL); } + chooseGpuProvider.setVisibility( + gfxstream || mode == GpuMode.OPENGL || mode == GpuMode.NATIVE + || mode == GpuMode.VULKAN ? VISIBLE : GONE); } - private void updateGpuVisibility() { - gpuOptions.setVisibility(swGpuEnabled.isChecked() ? VISIBLE : GONE); - if (!swGpuEnabled.isChecked()) { - if (chooseDisplayBackend.getSelectedItem() == DisplayBackend.VIRTIO_GPU) - chooseDisplayBackend.setSelectedItem(SIMPLEFB); + // The single value the daemon still reads. Every (mode, provider) pair the UI can produce + // maps onto one of the old names, so nothing is lost by keeping both written. + @NonNull + private static GpuApi toLegacyApi(GpuMode mode, GpuProvider provider) { + if (mode == GpuMode.NATIVE) return GpuApi.DRM2KGSL; + if (provider == null) return GpuApi.NONE; + switch (provider) { + case EGL: return GpuApi.EGL; + case GLES: return GpuApi.OPENGLES; + + // VULKAN mode on either renderer: gfxstream and venus both read the host ICD choice + // from these three (CrosvmBackendInstance.applyGfxstreamEnv). + case VK_SYSTEM: return GpuApi.VULKAN_SYSTEM; + case VK_TURNIP: return GpuApi.VULKAN_TURNIP; + case VK_PANVK: return GpuApi.VULKAN_PANVK; + default: return mode == GpuMode.VULKAN ? GpuApi.VULKAN : GpuApi.NONE; } - updateNativeDisplayVisibility(); } + private static boolean isVulkanProvider(Object p) { + return p == GpuProvider.VK_SYSTEM || p == GpuProvider.VK_TURNIP + || p == GpuProvider.VK_PANVK; + } + + /** + * The two screen blocks' visibility pass, run from every switch and picker under them. + * + *

    One pass rather than one per row: the renderer section belongs to the virtio-gpu switch + * above it and the blit row to that screen's exporter below it, so each row asking only about + * itself is how a stale combination stays on screen.

    + */ private void updateDisplayVisibility() { - displayOptions.setVisibility(swDisplayEnabled.isChecked() ? VISIBLE : GONE); + boolean gpuDevice = screenGpu0.isScreenEnabled(); + screenGpu0.updateVisibility(); + screenFb.updateVisibility(); + // Device off = the whole block folds to its enable switch: renderer, display and export + // sections all hide (the screen row hides its own two; the renderer section is ours). + rendererSection.setVisibility(gpuDevice ? VISIBLE : GONE); + // The GPU blit is no longer the native sink's step alone: the VNC sink dlopens the same + // driver for a headless blit of its own, so the row follows any binding that could climb + // to the GPU rung -- which for VNC means one whose ceiling has not been dropped to the CPU + // copy. With none of those, the row would name a driver nothing loads. It stays the same + // question the daemon's env predicate asks, asked of the live rows. + boolean anyBlitBinding = screenGpu0.isGpuBlitBinding() || screenFb.isGpuBlitBinding(); + chooseDisplayBlitProvider.setVisibility(anyBlitBinding ? VISIBLE : GONE); + } + + /** + * Enables or disables every control under [root]. + * + *

    Recursive because setEnabled on a ViewGroup does not reach its children: a block left + * looking greyed but still tappable is worse than one that was never greyed, since it takes + * input for a device that does not exist.

    + */ + private static void setRowsEnabled(@NonNull View root, boolean enabled) { + root.setEnabled(enabled); + if (!(root instanceof ViewGroup)) return; + var group = (ViewGroup) root; + for (int i = 0; i < group.getChildCount(); i++) + setRowsEnabled(group.getChildAt(i), enabled); } - private void updateDisplayDpiVisibility() { - var backend = chooseDisplayBackend.getSelectedItem(); - displayDpiOptions.setVisibility(backend != SIMPLEFB ? VISIBLE : GONE); - updateNativeDisplayVisibility(); + /** + * The rules crosvm would otherwise refuse the command line for, checked here so the refusal + * is a message in the editor rather than a VM that will not start. + * + *

    "One exporter per screen" needs no check: the schema keys the binding by screen, so it + * cannot be said twice. Two listeners on one port can be, and crosvm compares the effective + * ports -- so ports left unnamed are fine (the daemon hands out distinct ones), but two that + * name the same one are not. One listener per VNC binding again, now that the H.264 stream is + * served on the RFB port instead of beside it.

    + */ + private boolean validateScreens() { + var crosvm = parent.get("backend", VMBackend.DEFAULT) == VMBackend.CROSVM; + for (var row : new ScreenBindingRow[]{screenGpu0, screenFb}) { + if (!row.isScreenEnabled()) continue; + if (row.getExporter() == DisplayExporter.NATIVE && !crosvm) + return showValidateFailed(R.string.create_vm_error_native_display_only_crosvm); + if (row.getExporter() != DisplayExporter.VNC) continue; + if (!checkInputField(row.portField(), true, 1024, 65535)) return false; + } + return validateNoPortCollision(); } - private void updateNativeDisplayVisibility() { - // Native display rides on virtio-gpu (gfxstream) only. - boolean ok = swGpuEnabled.isChecked() - && chooseDisplayBackend.getSelectedItem() == DisplayBackend.VIRTIO_GPU; - swNativeDisplayEnabled.setVisibility(ok ? VISIBLE : GONE); - if (!ok) swNativeDisplayEnabled.setChecked(false); + /** + * The named ports, checked pairwise. Reported on the field that repeats one already claimed, so + * the error lands on the number the user would change rather than on the first one they typed. + */ + private boolean validateNoPortCollision() { + var fields = new ArrayList(); + var ports = new ArrayList(); + for (var row : new ScreenBindingRow[]{screenGpu0, screenFb}) { + if (!row.isScreenEnabled() || row.getExporter() != DisplayExporter.VNC) continue; + fields.add(row.portField()); + ports.add(row.typedVncPort()); + } + for (var i = 0; i < ports.size(); i++) { + if (ports.get(i) <= 0) continue; + for (var j = 0; j < i; j++) + if (ports.get(j).equals(ports.get(i))) { + fields.get(i).setError( + parent.getString(R.string.create_vm_error_vnc_port_conflict)); + return false; + } + } + return true; } - private void updateVncVisibility() { - vncOptions.setVisibility(swVncEnabled.isChecked() ? VISIBLE : GONE); + // Probing Vulkan touches a driver, so do it off the UI thread and report once via a hint. + // SYSTEM stays selected regardless of the outcome -- a miss just means crosvm will fall back + // to a CPU copy, which we tell the user about instead of letting it happen silently. A null + // result (no Vulkan / no device to ask) is treated as "unknown" and says nothing. + private void warnIfSystemBlitIncapable() { + new Thread(() -> { + String[] missing = VulkanBlitProbe.missingBlitExtensions(); + if (missing == null || missing.length == 0) return; + view.post(() -> { + if (chooseDisplayBlitProvider.getSelectedItem() == GpuBlitProvider.SYSTEM) + showHint(parent.getString(R.string.display_blit_system_missing_ext, + String.join("\n ", missing))); + }); + }, "vkprobe-blit").start(); } - private void updateVncPasswordVisibility() { - vncPasswordOptions.setVisibility(swVncPasswordAuth.isChecked() ? VISIBLE : GONE); + // VRAM allocation split: with guest-alloc (udmabuf) the guest owns a pre-sized pool + // (gpu_guest_pool_mb); otherwise host-visible memory is shared in at runtime, metered + // against gpu_vram_quota_mb. + // The host pool stays visible in both modes. + // + // The dynamic-vram switch is offered on both paths and gates a different set of fields on + // each: host-alloc grows host-visible memory past the pre-alloc pool (quota + fusion gate); + // guest-alloc grows the guest pool past its boot preallocation (prealloc / step / max + // grants). Both ultimately use normal RegisterMemory; Gunyah supplies SHARE/accept as its + // backend implementation rather than through a separate UI switch. + private void updateVramAllocVisibility() { + boolean gfxstream = chooseGpuBackend.getSelectedItem() == GPU_GFXSTREAM; + // The DRM native context has two: a guest pool every BO is allocated from, and a small + // host pool left holding only the per-context msm shmem rings. None of gfxstream's other + // plumbing applies -- vram-limit and the fusion size gate have gfxstream-only consumers. + // Read the mode only under virglrenderer: 2D clears the mode picker and reading it would + // throw "Items not set". drm2kgsl only exists as virglrenderer's NATIVE mode anyway. + boolean drm2kgsl = chooseGpuBackend.getSelectedItem() == GpuBackend.GPU_VIRGLRENDERER + && chooseGpuMode.getSelectedItem() == GpuMode.NATIVE; + // Venus (virglrenderer + VULKAN) also allocates every BO from the guest pool; its host + // pool (venus-host-mb) holds the venus transport shmems and is tunable via its own field. + boolean venus = chooseGpuBackend.getSelectedItem() == GpuBackend.GPU_VIRGLRENDERER + && chooseGpuMode.getSelectedItem() == GpuMode.VULKAN; + boolean udmabuf = gfxstream && swGpuUdmabuf.isChecked(); + vramSettings.setVisibility(gfxstream || drm2kgsl || venus ? VISIBLE : GONE); + // The three host pools each hold only a renderer's command-stream/transport buffers, so the + // UI labels them all "command buffer pool size"; exactly one is shown for the active mode. + tilGpuDrm2KgslPoolMb.setVisibility(drm2kgsl ? VISIBLE : GONE); + swGpuUdmabuf.setVisibility(gfxstream ? VISIBLE : GONE); + tilGpuHostPoolMb.setVisibility(gfxstream ? VISIBLE : GONE); + tilGpuVenusPoolMb.setVisibility(venus ? VISIBLE : GONE); + // The guest-allocated pool is the same region and the same flag for both renderers -- the + // guest driver keeps one allocator -- so it is offered wherever guest-alloc is in use: + // gfxstream with udmabuf on, and the DRM native context always (every BO comes from it). + boolean guestAlloc = (udmabuf || drm2kgsl || venus) && !hostVisibleRam(); + tilGpuGuestPoolMb.setVisibility(guestAlloc ? VISIBLE : GONE); + boolean hostAlloc = gfxstream && !udmabuf; + // One of the two is always true whenever the VRAM block itself is shown, so the switch + // sits under the pool sizes on every path. + swGpuDynamicVram.setVisibility(hostAlloc || guestAlloc ? VISIBLE : GONE); + dynamicVramOptions.setVisibility( + (hostAlloc || guestAlloc) && swGpuDynamicVram.isChecked() ? VISIBLE : GONE); + dynamicVramHostOptions.setVisibility(hostAlloc ? VISIBLE : GONE); + dynamicVramGuestOptions.setVisibility(guestAlloc ? VISIBLE : GONE); } } diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/network/VMEditNetworkTab.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/network/VMEditNetworkTab.java index c1b775d84..687461dd4 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/network/VMEditNetworkTab.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/network/VMEditNetworkTab.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.vm.edit.network; import static java.util.Objects.requireNonNull; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/network/VMNetEditAdapter.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/network/VMNetEditAdapter.java index fcb55ebbc..38cbabb99 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/network/VMNetEditAdapter.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/network/VMNetEditAdapter.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.vm.edit.network; import static android.view.View.GONE; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/network/VMNetEditViewHolder.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/network/VMNetEditViewHolder.java index 32d0721c4..7845e915f 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/network/VMNetEditViewHolder.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/network/VMNetEditViewHolder.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.vm.edit.network; import android.view.View; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/storage/VMEditStorageTab.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/storage/VMEditStorageTab.java index 5c516aa55..d843746bc 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/storage/VMEditStorageTab.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/storage/VMEditStorageTab.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.vm.edit.storage; import static android.app.Activity.RESULT_OK; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/storage/dir/VMSharedDirEditAdapter.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/storage/dir/VMSharedDirEditAdapter.java index ee99894f2..3a7175781 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/storage/dir/VMSharedDirEditAdapter.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/storage/dir/VMSharedDirEditAdapter.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.vm.edit.storage.dir; import static cn.classfun.droidvm.lib.store.enums.Enums.optEnum; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/storage/dir/VMSharedDirEditViewHolder.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/storage/dir/VMSharedDirEditViewHolder.java index fa8588127..2cdf88710 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/storage/dir/VMSharedDirEditViewHolder.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/storage/dir/VMSharedDirEditViewHolder.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.vm.edit.storage.dir; import android.text.TextWatcher; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/storage/disk/VMDiskEditAdapter.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/storage/disk/VMDiskEditAdapter.java index c8271fdc6..157c056d9 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/storage/disk/VMDiskEditAdapter.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/storage/disk/VMDiskEditAdapter.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.vm.edit.storage.disk; import static android.widget.Toast.LENGTH_SHORT; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/storage/disk/VMDiskEditViewHolder.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/storage/disk/VMDiskEditViewHolder.java index b3fcbe2c8..e5917b52c 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/storage/disk/VMDiskEditViewHolder.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/storage/disk/VMDiskEditViewHolder.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.vm.edit.storage.disk; import android.text.TextWatcher; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/info/ConsoleButton.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/info/ConsoleButton.java index 5cfda47be..ff2c28684 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/info/ConsoleButton.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/info/ConsoleButton.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.vm.info; import static android.widget.Toast.LENGTH_SHORT; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/info/VMInfoActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/info/VMInfoActivity.java index c5b822933..0afa0fffd 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/info/VMInfoActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/info/VMInfoActivity.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.vm.info; import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/exports/VMPkgExportActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/exports/VMPkgExportActivity.java index 74650153b..f173cc23d 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/exports/VMPkgExportActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/exports/VMPkgExportActivity.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.vm.pkg.exports; import static android.view.View.GONE; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/exports/VMPkgExportDiskAdapter.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/exports/VMPkgExportDiskAdapter.java index 11d6522ce..2c40e6ccf 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/exports/VMPkgExportDiskAdapter.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/exports/VMPkgExportDiskAdapter.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.vm.pkg.exports; import static android.view.View.GONE; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/imports/NetworkImportMode.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/imports/NetworkImportMode.java index 4b89116ef..c41205e16 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/imports/NetworkImportMode.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/imports/NetworkImportMode.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.vm.pkg.imports; import androidx.annotation.NonNull; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/imports/VMPkgImportActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/imports/VMPkgImportActivity.java index 9689b5501..9db922c36 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/imports/VMPkgImportActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/imports/VMPkgImportActivity.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.vm.pkg.imports; import static android.view.View.GONE; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/imports/VMPkgImportDiskAdapter.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/imports/VMPkgImportDiskAdapter.java index a7b67fab7..9d06d3f02 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/imports/VMPkgImportDiskAdapter.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/imports/VMPkgImportDiskAdapter.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.vm.pkg.imports; import static android.view.View.GONE; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/imports/VMPkgImportNetworkAdapter.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/imports/VMPkgImportNetworkAdapter.java index fc9a8d293..f3aa62a9c 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/imports/VMPkgImportNetworkAdapter.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/imports/VMPkgImportNetworkAdapter.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.vm.pkg.imports; import static android.view.View.GONE; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/widgets/container/CardItemAdapter.java b/app/src/main/java/cn/classfun/droidvm/ui/widgets/container/CardItemAdapter.java index 733ba3906..621c14b88 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/widgets/container/CardItemAdapter.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/widgets/container/CardItemAdapter.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.widgets.container; import android.annotation.SuppressLint; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/widgets/container/CardItemListView.java b/app/src/main/java/cn/classfun/droidvm/ui/widgets/container/CardItemListView.java index d65d41b45..5df93d365 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/widgets/container/CardItemListView.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/widgets/container/CardItemListView.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.widgets.container; import android.content.Context; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/widgets/container/CollapsibleContainer.java b/app/src/main/java/cn/classfun/droidvm/ui/widgets/container/CollapsibleContainer.java index 616138e49..13f8c2297 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/widgets/container/CollapsibleContainer.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/widgets/container/CollapsibleContainer.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.widgets.container; import android.content.Context; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/widgets/row/ChooseRowWidget.java b/app/src/main/java/cn/classfun/droidvm/ui/widgets/row/ChooseRowWidget.java index 1a5e4b0c8..f3291123d 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/widgets/row/ChooseRowWidget.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/widgets/row/ChooseRowWidget.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.widgets.row; import android.content.Context; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/widgets/row/DropdownRowWidget.java b/app/src/main/java/cn/classfun/droidvm/ui/widgets/row/DropdownRowWidget.java index 8bb604fa2..448497ad3 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/widgets/row/DropdownRowWidget.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/widgets/row/DropdownRowWidget.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.widgets.row; import android.content.Context; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/widgets/row/SwitchRowWidget.java b/app/src/main/java/cn/classfun/droidvm/ui/widgets/row/SwitchRowWidget.java index 5cd2dc8d6..272f260a8 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/widgets/row/SwitchRowWidget.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/widgets/row/SwitchRowWidget.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.widgets.row; import android.annotation.SuppressLint; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/widgets/row/TextInputRowWidget.java b/app/src/main/java/cn/classfun/droidvm/ui/widgets/row/TextInputRowWidget.java index 03847de12..29349ec43 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/widgets/row/TextInputRowWidget.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/widgets/row/TextInputRowWidget.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.widgets.row; import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/widgets/row/TextRowWidget.java b/app/src/main/java/cn/classfun/droidvm/ui/widgets/row/TextRowWidget.java index c6d7fc085..d064b3308 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/widgets/row/TextRowWidget.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/widgets/row/TextRowWidget.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.widgets.row; import android.content.Context; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/widgets/tools/DownloadWidget.java b/app/src/main/java/cn/classfun/droidvm/ui/widgets/tools/DownloadWidget.java index 8e56fa275..da7870f82 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/widgets/tools/DownloadWidget.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/widgets/tools/DownloadWidget.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.widgets.tools; import static cn.classfun.droidvm.lib.utils.StringUtils.formatDuration; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/widgets/tools/KernelAnalysisWidget.java b/app/src/main/java/cn/classfun/droidvm/ui/widgets/tools/KernelAnalysisWidget.java index 7bcffd49c..ca8936e95 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/widgets/tools/KernelAnalysisWidget.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/widgets/tools/KernelAnalysisWidget.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.widgets.tools; import static android.view.View.GONE; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/widgets/tools/PickerButtonWidget.java b/app/src/main/java/cn/classfun/droidvm/ui/widgets/tools/PickerButtonWidget.java index 6547b76bc..597abc709 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/widgets/tools/PickerButtonWidget.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/widgets/tools/PickerButtonWidget.java @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.widgets.tools; import android.content.Context; diff --git a/app/src/main/res/layout/partial_vm_edit_graphics.xml b/app/src/main/res/layout/partial_vm_edit_graphics.xml index 1f66ed78d..2c62c8006 100644 --- a/app/src/main/res/layout/partial_vm_edit_graphics.xml +++ b/app/src/main/res/layout/partial_vm_edit_graphics.xml @@ -1,4 +1,15 @@ - + + android:title="@string/create_vm_category_screen_gpu0"> - - - - - - - - - - - - - - + android:text="@string/create_vm_screen_enabled" /> - - - - - + android:id="@+id/renderer_section" + android:title="@string/create_vm_category_renderer" + app:cc_expanded="true"> + + android:orientation="vertical"> - + + + + android:icon="@drawable/ic_application_cog" + android:text="@string/create_vm_gpu_mode" /> - + - - + android:title="@string/create_vm_category_vram" + app:cc_expanded="true"> - + android:icon="@drawable/ic_memory" + android:text="@string/create_vm_gpu_udmabuf" /> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + android:title="@string/create_vm_category_gpu_cgroup"> - + android:icon="@drawable/ic_gpu" + android:text="@string/create_vm_gpu_cgroup" /> - + + + + + + + + + + + + + + + + + - - - + - + - + - + - + - - + - + + + + + + android:title="@string/create_vm_category_vpu"> + + android:icon="@drawable/ic_video" + android:text="@string/create_vm_vpu_enabled" /> - - + + + + android:hint="@string/create_vm_vpu_host_pool_mb"> - + android:inputType="number" + android:text="256" + tools:ignore="HardcodedText,VisualLintTextFieldSize" /> - + - + + + android:layout_marginBottom="8dp" + android:hint="@string/create_vm_vpu_guest_pool_mb"> + android:text="128" + tools:ignore="HardcodedText,VisualLintTextFieldSize" /> + - - - - - - - - - - - - - - + android:text="@string/create_vm_vpu_note" + android:textSize="12sp" /> + From d8eb93cd2b8bb295f06c3ed4f310140e3e0a336d Mon Sep 17 00:00:00 2001 From: HuJK Date: Sun, 30 Aug 2026 13:37:07 +0800 Subject: [PATCH 127/182] ci: three source checks, and a build that can unit-test a class that logs check_arity.py walls off the constructors and methods that had grown past the point where a caller can be read: a positional list long enough that swapping two arguments of the same type still compiles. check_vector_icons.py holds the drawables to one shape so an icon added in a hurry does not arrive at a different optical size than the row it sits in. check_string_concat gets stricter, and the two allow-lists carry the deliberate exceptions rather than leaving the checks off. isReturnDefaultValues lets a unit test cover a class that logs. Keeping every testable class free of android.util.Log stopped being tenable once the classes worth testing were the ones whose whole subject is a socket and a thread outliving the object that owned them, and which say so out loud when they do. Nothing asserts on a stub's return value; the stubs only let the class under test be built. --- .github/workflows/checks.yml | 6 + .gitignore | 4 + app/build.gradle.kts | 10 ++ app/src/main/assets/prebuilts | 2 +- scripts/ci/arity_allow.txt | 22 ++++ scripts/ci/ascii_allow.txt | 3 + scripts/ci/check_arity.py | 192 ++++++++++++++++++++++++++++++ scripts/ci/check_string_concat.py | 21 ++-- scripts/ci/check_vector_icons.py | 73 ++++++++++++ 9 files changed, 325 insertions(+), 8 deletions(-) create mode 100644 scripts/ci/arity_allow.txt create mode 100755 scripts/ci/check_arity.py create mode 100755 scripts/ci/check_vector_icons.py diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 78e71579d..0318d0833 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -33,3 +33,9 @@ jobs: - name: Use fmt() instead of string concatenation run: python3 scripts/ci/check_string_concat.py + + - name: Icons are vector drawables + run: python3 scripts/ci/check_vector_icons.py + + - name: No signature over the parameter limit + run: python3 scripts/ci/check_arity.py diff --git a/.gitignore b/.gitignore index 2625919c3..478500f90 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,7 @@ local.properties # Terminal font: build.sh fetches the ~2.3MB Maple Mono NL NF TTF on first # build; only the OFL license text under app/src/main/assets/fonts is committed. /app/src/main/assets/fonts/*.ttf + +# Python bytecode from the scripts/ci checks. +__pycache__/ +*.pyc diff --git a/app/build.gradle.kts b/app/build.gradle.kts index aac83d6e7..8e334528c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -98,6 +98,16 @@ android { aidl = true buildConfig = true } + testOptions { + unitTests { + // Lets a unit test cover a class that logs. The alternative -- keeping every testable + // class free of android.util.Log -- stopped being tenable at the H.264 side channel, + // whose whole subject is a socket and a thread outliving the object that owned them, + // and which says so out loud when they do. Nothing here asserts on a stub's return + // value; the stubs are only there so the class under test can be built at all. + isReturnDefaultValues = true + } + } packaging { jniLibs { // Extract native libs to a real on-disk dir so lbx (shipped as diff --git a/app/src/main/assets/prebuilts b/app/src/main/assets/prebuilts index 2c31cecde..8631ea61e 160000 --- a/app/src/main/assets/prebuilts +++ b/app/src/main/assets/prebuilts @@ -1 +1 @@ -Subproject commit 2c31cecdedb5110e8861b8b698ba5b538725cfb7 +Subproject commit 8631ea61eb07acd3ebfd17553b65797fc28d5149 diff --git a/scripts/ci/arity_allow.txt b/scripts/ci/arity_allow.txt new file mode 100644 index 000000000..31e437b51 --- /dev/null +++ b/scripts/ci/arity_allow.txt @@ -0,0 +1,22 @@ +# Signatures that already exceeded check_arity.py's parameter limit when the +# check was introduced. This is a debt list, not a blessing: new code must not +# grow it, and an entry should be deleted the moment its signature is reworked +# (group the related parameters into an object, or split the method). +# +# One ":" per line; the entry +# covers every overload of that name in that file. Blank lines and '#' comments +# are ignored. + +# A DNAT rule passed as eight positional values; wants a rule object. +app/src/main/java/cn/classfun/droidvm/daemon/network/backend/FirewallHelper.java:applyDnat +app/src/main/java/cn/classfun/droidvm/daemon/network/backend/FirewallHelper.java:removeDnat +app/src/main/java/cn/classfun/droidvm/daemon/network/backend/iptables/IptablesBackend.java:applyDnat +app/src/main/java/cn/classfun/droidvm/daemon/network/backend/iptables/IptablesBackend.java:removeDnat + +# Huge-page process rows: a data holder built positionally (10 and 12 wide). +app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcess.java:HugePageProcess +app/src/main/java/cn/classfun/droidvm/ui/hugepage/HugePageProcessActivity.java:showResult +app/src/main/java/cn/classfun/droidvm/ui/hugepage/SegmentedBar.java:setStorage + +# Import progress reported as ten loose counters. +app/src/main/java/cn/classfun/droidvm/ui/vm/pkg/imports/VMPkgImportActivity.java:onProgress diff --git a/scripts/ci/ascii_allow.txt b/scripts/ci/ascii_allow.txt index 503a8e61a..6774fb8e4 100644 --- a/scripts/ci/ascii_allow.txt +++ b/scripts/ci/ascii_allow.txt @@ -13,6 +13,9 @@ app/src/main/res/values-*/strings.xml # Privacy policy, one file per language. app/src/main/assets/privacy/*.txt +# In-app description pages: prose, with an en/zh-CN/zh-TW block per page. +app/src/main/assets/descr/*.html + # Localized data: repo-listing display names and language endonyms. app/src/main/assets/data/repo.yaml app/src/main/assets/data/languages.yaml diff --git a/scripts/ci/check_arity.py b/scripts/ci/check_arity.py new file mode 100755 index 000000000..0141a9bf4 --- /dev/null +++ b/scripts/ci/check_arity.py @@ -0,0 +1,192 @@ +#!/usr/bin/env python3 +"""Fail if a Java method or constructor declares too many parameters. + +A call site cannot pass more arguments than the signature declares, so the +place to hold the line is the declaration -- and only there. Checking call +sites too would flag every varargs helper (`fmt("%s %s ...", a, b, c, d, e)`, +`runList("ip", "addr", "add", ...)`), where a long argument list is the point +and the signature is still two parameters wide. + +The limit is 7, Checkstyle's ParameterNumber default. Past that, callers +start passing arguments positionally by luck: bundle the related ones into a +small object, or split the method. + +Two escape hatches: + - a trailing `// arity-ok` comment on the line the signature starts on, for a + deliberate exception in new code; + - scripts/ci/arity_allow.txt, one `path:name` per line, for signatures that + predate this check. That file is a debt list, not a blessing: new code must + not grow it, and an entry should disappear when its signature is reworked. + +Not a full parser: it masks comments and string/char literals, then reads the +`name(` sites whose text before them looks like ` name` and +whose text after them starts a body (`{`, `;`, or `throws ...`). Generic +parameter types (`Map`) are counted as one parameter. + +Exit status is 0 when clean, 1 when any violation is found. Run from the repo +root, or pass the repo root as argv[1]. +""" +import os +import re +import subprocess +import sys + +LIMIT = 7 +SUPPRESS = "arity-ok" +ALLOWLIST = "scripts/ci/arity_allow.txt" +SOURCES = "app/src" + +# Control-flow keywords that also read as `name(`. +KEYWORDS = {"if", "for", "while", "switch", "catch", "synchronized", "return", + "assert", "do", "else", "try", "case", "new", "throw", "yield", + "instanceof"} +# Words that can precede `name(` and still leave it an expression, not a signature. +NOT_A_TYPE = {"new", "return", "throw", "else", "instanceof", "case", "assert", "yield"} +DECL_BEFORE = re.compile(r"[\w>\]]\s+$") +# A constructor with no modifier has no type before it, only the end of the +# previous member -- recognised by the capitalised name in member position. +CTOR_BEFORE = re.compile(r"[{};]\s+$") +LAST_WORD = re.compile(r"(\w+)\s+$") +TAIL_IS_BODY = re.compile(r"\s*(throws [\w.,\s]+)?[{;]") +CALL_SITE = re.compile(r"\b([A-Za-z_$][\w$]*)\s*\(") + + +def mask(src): + """Blank out comments and string/char literals, keeping every offset and + newline in place so line numbers and slices stay true to the original.""" + out = list(src) + i, n = 0, len(src) + while i < n: + two = src[i:i + 2] + if two == "//": + j = src.find("\n", i) + j = n if j < 0 else j + for k in range(i, j): + out[k] = " " + i = j + elif two == "/*": + j = src.find("*/", i + 2) + j = n if j < 0 else j + 2 + for k in range(i, min(j, n)): + if src[k] != "\n": + out[k] = " " + i = j + elif src[i] in "\"'": + quote, j = src[i], i + 1 + while j < n: + if src[j] == "\\": + j += 2 + continue + if src[j] == quote: + j += 1 + break + j += 1 + for k in range(i, min(j, n)): + out[k] = " " + i = j + else: + i += 1 + return "".join(out) + + +def count_params(text): + """Top-level commas + 1. Nested (), [], {} and generics all shield commas.""" + if not text.strip(): + return 0 + count, depth = 1, 0 + for ch in text: + if ch in "([{<": + depth += 1 + elif ch in ")]}>": + depth = max(0, depth - 1) + elif ch == "," and depth == 0: + count += 1 + return count + + +def declarations(src): + """Yield (line, name, param_count) for every method/constructor signature.""" + masked = mask(src) + for m in CALL_SITE.finditer(masked): + name = m.group(1) + if name in KEYWORDS: + continue + before = masked[:m.start()] + if not DECL_BEFORE.search(before): + if not (name[:1].isupper() and CTOR_BEFORE.search(before)): + continue + word = LAST_WORD.search(before) + if word and word.group(1) in NOT_A_TYPE: + continue + start = m.end() - 1 + depth, i = 0, start + while i < len(masked): + if masked[i] == "(": + depth += 1 + elif masked[i] == ")": + depth -= 1 + if depth == 0: + break + i += 1 + else: + continue + if not TAIL_IS_BODY.match(masked[i + 1:i + 120]): + continue + yield masked.count("\n", 0, m.start()) + 1, name, count_params(masked[start + 1:i]) + + +def load_allowlist(root): + path = os.path.join(root, ALLOWLIST) + allowed = set() + if not os.path.exists(path): + return allowed + with open(path, encoding="utf-8") as f: + for line in f: + line = line.split("#", 1)[0].strip() + if line: + allowed.add(line) + return allowed + + +def java_files(root): + out = subprocess.run( + ["git", "-C", root, "ls-files", SOURCES], + capture_output=True, text=True, check=True).stdout + return [line for line in out.splitlines() if line.endswith(".java")] + + +def check(root): + allowed = load_allowlist(root) + problems = [] + for rel in java_files(root): + with open(os.path.join(root, rel), encoding="utf-8") as f: + src = f.read() + lines = src.split("\n") + for line, name, count in declarations(src): + if count <= LIMIT: + continue + if SUPPRESS in lines[line - 1]: + continue + if "%s:%s" % (rel, name) in allowed: + continue + problems.append((rel, line, name, count)) + return problems + + +def main(argv): + root = argv[1] if len(argv) > 1 else "." + problems = check(root) + for rel, line, name, count in problems: + print("%s:%d: %s() declares %d parameters (limit %d)" + % (rel, line, name, count, LIMIT)) + if problems: + print("\nFAIL: %d signature(s) over the parameter limit." % len(problems)) + print("Group the related parameters into an object, or split the method.") + print("For a deliberate exception, add a trailing '// arity-ok' comment.") + return 1 + print("OK: no signature declares more than %d parameters." % LIMIT) + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/scripts/ci/check_string_concat.py b/scripts/ci/check_string_concat.py index e6354feef..9e95bd619 100644 --- a/scripts/ci/check_string_concat.py +++ b/scripts/ci/check_string_concat.py @@ -5,6 +5,9 @@ a runtime expression (`"x=" + v`, `v + " done"`), which reads better as `fmt("x=%s", v)`. Deliberately NOT flagged: - `+` inside a string literal ("wlan+", "%s+", "\\s+") -- masked out first; + - char literals: `(char) ('a' + i)` is integer arithmetic, not string building, + so a char literal is masked to its own sentinel and never counts as "the + string side" of a `+`; - literal + literal ("a" + "b") -- a compile-time constant, fmt adds nothing; - `++` and numeric `+` with no string operand; - a `+=` append with a SINGLE `+` (`s += "x" + y`) -- appending one piece to @@ -32,11 +35,14 @@ def mask(src): - """Replace comments with spaces and each string/char literal with one STR - sentinel (\x01), preserving newlines so line numbers stay accurate.""" + """Replace comments with spaces, each string literal with one STR sentinel + (\x01) and each char literal with a CHR sentinel (\x02), preserving newlines + so line numbers stay accurate. The two differ because only a string literal + makes a `+` string building: `'a' + i` is arithmetic on code points.""" out = [] i, n = 0, len(src) STR = "\x01" + CHR = "\x02" while i < n: c = src[i] two = src[i:i + 2] @@ -60,7 +66,7 @@ def mask(src): j += 1 break j += 1 - out.append(STR) + out.append(STR if c == '"' else CHR) i = j else: out.append(c) @@ -68,11 +74,12 @@ def mask(src): return "".join(out) -# Significant tokens: a string literal (\x01), '+=' (marks an append statement), -# a value (identifier / number / closing bracket = end of a sub-expression), a -# '+', or a statement boundary (; { }) that scopes the '+=' exemption. +# Significant tokens: a string literal (\x01), a char literal (\x02, a value like +# any other), '+=' (marks an append statement), a value (identifier / number / +# closing bracket = end of a sub-expression), a '+', or a statement boundary +# (; { }) that scopes the '+=' exemption. TOKEN = re.compile(r""" - \x01 # string literal + \x01 | \x02 # string literal / char literal | \+\+ | \+= # compound operators (+= = append) | \+ # the concatenation operator | [A-Za-z_$][A-Za-z0-9_$]* | [0-9][\w.]* # identifier / number diff --git a/scripts/ci/check_vector_icons.py b/scripts/ci/check_vector_icons.py new file mode 100755 index 000000000..b0a58bbb0 --- /dev/null +++ b/scripts/ci/check_vector_icons.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +"""Fail if an icon ships as a bitmap instead of a vector drawable. + +Every icon the app draws must be an XML vector: it stays sharp at any density, +tints from the theme (`?attr/colorOnSurface`), and costs one small file instead +of five PNG buckets. So `res/drawable*` may hold XML only -- vectors, shapes, +selectors, layer-lists -- and no XML in there may wrap a raster through +`` either, which would smuggle one in the back door. + +The one exception is the launcher icon: Android still hands `res/mipmap-*` to +legacy launchers as a bitmap, and the adaptive-icon foreground/background are +generated with it. So `mipmap-*` may carry raster files, but only ones named +`ic_launcher*`; anything else belongs in `drawable/` as a vector. + +Scope is the git index (`git ls-files`), so build outputs never count. Exit +status is 0 when clean, 1 when any violation is found. + +Usage: run from the repo root, or pass the repo root as argv[1]. +""" +import os +import re +import subprocess +import sys + +RES = "app/src/main/res" +RASTER_EXT = {".png", ".webp", ".jpg", ".jpeg", ".gif", ".bmp", ".9.png"} +LAUNCHER_PREFIX = "ic_launcher" +# and friends: an XML wrapper around a raster. +BITMAP_TAG = re.compile(r"<\s*bitmap\b") + + +def tracked_files(root): + out = subprocess.run( + ["git", "-C", root, "ls-files", RES], + capture_output=True, text=True, check=True).stdout + return [line for line in out.splitlines() if line.strip()] + + +def check(root): + problems = [] + for rel in tracked_files(root): + parts = rel.split("/") + if len(parts) < 2: + continue + folder, name = parts[-2], parts[-1] + ext = os.path.splitext(name)[1].lower() + if folder.startswith("drawable"): + if ext != ".xml": + problems.append((rel, "raster in drawable/; ship an XML vector")) + continue + with open(os.path.join(root, rel), encoding="utf-8", errors="replace") as f: + if BITMAP_TAG.search(f.read()): + problems.append((rel, " wraps a raster; ship an XML vector")) + elif folder.startswith("mipmap"): + if ext in RASTER_EXT and not name.startswith(LAUNCHER_PREFIX): + problems.append((rel, "only ic_launcher* may be a raster in mipmap/")) + return problems + + +def main(argv): + root = argv[1] if len(argv) > 1 else "." + problems = check(root) + for rel, why in problems: + print("%s: %s" % (rel, why)) + if problems: + print("\nFAIL: %d non-vector icon(s)." % len(problems)) + return 1 + print("OK: every icon is a vector drawable.") + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) From 2c42bcbc152e3804bd6fa03241cf10b38394adef Mon Sep 17 00:00:00 2001 From: HuJK Date: Sun, 30 Aug 2026 13:37:09 +0800 Subject: [PATCH 128/182] display: one framework behind the VNC and native screens The two display screens had grown the same features twice and disagreed about half of them. A DisplaySource interface now names what a screen consumes (a Surface for the native path, a bitmap for VNC), and the viewport and chrome controllers hold the parts that were duplicated: the fit/zoom/pan arithmetic, the toolbar and its auto-hide, the resolution-follow that resizes the guest to the window it is being shown in. Input is routed once. Pointer motion is coalesced before it reaches the guest, gestures are translated in one place with an explicit mode (touch, mouse, tablet) instead of each screen guessing, and the keyboard grew what a keyboard needs: real key holds rather than tap-to-toggle, a laptop layout, a physical keyboard view, and an extra-keys row that fits on a phone. Scan codes are generated from one table -- the letters used to be wrong, so typing produced different characters than the key that was pressed. Two failure modes are fixed here because they belong to the framework rather than to either screen. The binder wait for the display root service could leak threads and retried without a cap, so a VM that had already stopped left the app spinning; it now waits once and gives up with the VM. And a crosvm that restarts under a live display used to leave the surfaces pointing at a dead process -- the reconnect rebuilds them. Co-authored-by: lateautumn233 --- .../daemon/display/NativeDisplayBinder.java | 73 +- .../vm/backend/CrosvmBackendInstance.java | 30 +- .../droidvm/lib/perf/SystemGestureGuard.java | 94 ++ .../droidvm/lib/store/vm/NativeDisplay.java | 10 +- .../classfun/droidvm/lib/ui/MaterialMenu.java | 23 + .../vm/display/base/BaseExtraKeysAdapter.java | 24 +- .../vm/display/base/DaemonDisplayAttach.java | 147 +++ .../display/base/DisplayChromeController.java | 153 +++ .../display/base/DisplayExtraKeysPanel.java | 316 +++--- .../vm/display/base/DisplayInputBackend.java | 36 + .../display/base/DisplayKeyboardMenuRow.java | 61 ++ .../base/DisplayPhysicalKeyboardView.java | 330 ++++++ .../ui/vm/display/base/DisplaySource.java | 68 ++ .../base/DisplayViewportController.java | 262 +++++ .../ui/vm/display/base/HoldKeyGroup.java | 142 +++ .../droidvm/ui/vm/display/base/InputMode.java | 51 + .../ui/vm/display/base/KeyListener.java | 16 +- .../ui/vm/display/base/KeyboardMode.java | 48 + .../base/PointerGestureTranslator.java | 467 +++++++++ .../vm/display/base/ViewHeightAnimator.java | 97 ++ .../droidvm/ui/vm/display/base/X11Keymap.java | 7 +- .../display/CursorPositionStream.java | 137 +++ .../display/DisplayProvider.java | 293 +++++- .../display/NativeSurfaceSource.java | 74 ++ .../display/VMNativeDisplayActivity.java | 857 +++++++++++++--- .../nativedisplay/input/EvdevEncoder.java | 146 +++ .../nativedisplay/input/InputForwarder.java | 312 +++++- .../nativedisplay/input/KeyCodeMapper.java | 66 ++ .../input/NativeExtraKeysPanel.java | 26 +- .../input/TouchScaleCalculator.java | 22 +- .../vm/display/vnc/base/BaseVncActivity.java | 40 +- .../vnc/display/VMVncDisplayActivity.java | 963 +++++++++--------- .../display/vnc/display/VncBitmapSource.java | 51 + .../display/vnc/input/VncExtraKeysPanel.java | 39 +- app/src/main/res/drawable/extra_key_bg.xml | 8 + .../main/res/drawable/ic_key_backspace.xml | 10 + app/src/main/res/drawable/ic_key_enter.xml | 10 + app/src/main/res/drawable/ic_key_keyboard.xml | 13 + app/src/main/res/drawable/ic_key_shift.xml | 10 + app/src/main/res/drawable/ic_key_space.xml | 10 + app/src/main/res/drawable/ic_key_tab.xml | 10 + app/src/main/res/drawable/ic_key_windows.xml | 10 + .../main/res/drawable/ic_keyboard_close.xml | 10 + app/src/main/res/drawable/ic_keyboard_phy.xml | 9 + .../main/res/drawable/ic_tablet_stylus.xml | 21 + app/src/main/res/drawable/ic_touchscreen.xml | 21 + .../res/layout/activity_vm_native_display.xml | 32 +- .../res/layout/activity_vm_vnc_display.xml | 7 + .../res/layout/view_input_mode_toggle.xml | 56 + .../res/layout/view_keyboard_mode_toggle.xml | 57 ++ .../res/layout/widget_display_extra_keys.xml | 301 +++--- .../res/menu/menu_native_display_menu.xml | 15 +- .../main/res/menu/menu_vnc_display_menu.xml | 15 - app/src/main/res/values-zh-rCN/strings.xml | 9 + app/src/main/res/values-zh-rTW/strings.xml | 9 + app/src/main/res/values/colors.xml | 2 + app/src/main/res/values/ids.xml | 5 + app/src/main/res/values/strings.xml | 23 +- app/src/main/res/values/themes.xml | 14 +- .../base/DisplayChromeControllerTest.java | 211 ++++ .../base/DisplayViewportControllerTest.java | 238 +++++ 61 files changed, 5463 insertions(+), 1154 deletions(-) create mode 100644 app/src/main/java/cn/classfun/droidvm/lib/perf/SystemGestureGuard.java create mode 100644 app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/DaemonDisplayAttach.java create mode 100644 app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/DisplayChromeController.java create mode 100644 app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/DisplayInputBackend.java create mode 100644 app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/DisplayKeyboardMenuRow.java create mode 100644 app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/DisplayPhysicalKeyboardView.java create mode 100644 app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/DisplaySource.java create mode 100644 app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/DisplayViewportController.java create mode 100644 app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/HoldKeyGroup.java create mode 100644 app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/InputMode.java create mode 100644 app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/KeyboardMode.java create mode 100644 app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/PointerGestureTranslator.java create mode 100644 app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/ViewHeightAnimator.java create mode 100644 app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/display/CursorPositionStream.java create mode 100644 app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/display/NativeSurfaceSource.java create mode 100644 app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/display/VncBitmapSource.java create mode 100644 app/src/main/res/drawable/extra_key_bg.xml create mode 100644 app/src/main/res/drawable/ic_key_backspace.xml create mode 100644 app/src/main/res/drawable/ic_key_enter.xml create mode 100644 app/src/main/res/drawable/ic_key_keyboard.xml create mode 100644 app/src/main/res/drawable/ic_key_shift.xml create mode 100644 app/src/main/res/drawable/ic_key_space.xml create mode 100644 app/src/main/res/drawable/ic_key_tab.xml create mode 100644 app/src/main/res/drawable/ic_key_windows.xml create mode 100644 app/src/main/res/drawable/ic_keyboard_close.xml create mode 100644 app/src/main/res/drawable/ic_keyboard_phy.xml create mode 100644 app/src/main/res/drawable/ic_tablet_stylus.xml create mode 100644 app/src/main/res/drawable/ic_touchscreen.xml create mode 100644 app/src/main/res/layout/view_input_mode_toggle.xml create mode 100644 app/src/main/res/layout/view_keyboard_mode_toggle.xml create mode 100644 app/src/main/res/values/ids.xml create mode 100644 app/src/test/java/cn/classfun/droidvm/ui/vm/display/base/DisplayChromeControllerTest.java create mode 100644 app/src/test/java/cn/classfun/droidvm/ui/vm/display/base/DisplayViewportControllerTest.java diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/display/NativeDisplayBinder.java b/app/src/main/java/cn/classfun/droidvm/daemon/display/NativeDisplayBinder.java index 65056c6c1..25b54b144 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/display/NativeDisplayBinder.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/display/NativeDisplayBinder.java @@ -18,6 +18,7 @@ import cn.classfun.droidvm.daemon.server.ServerContext; import cn.classfun.droidvm.display.INativeDisplayRootService; import cn.classfun.droidvm.lib.store.vm.NativeDisplay; +import cn.classfun.droidvm.lib.store.vm.VMState; /** * Daemon-hosted native-display broker. The daemon already runs as root, so it does both jobs the @@ -36,6 +37,10 @@ public final class NativeDisplayBinder { private static final String TAG = "NativeDisplayBinder"; + /** How long one lookup keeps looking, and how often it looks. */ + private static final long WAIT_TOTAL_MS = 5000; + private static final long WAIT_POLL_MS = 200; + private static INativeDisplayRootService.Stub binder; private NativeDisplayBinder() { @@ -54,7 +59,7 @@ private static INativeDisplayRootService.Stub createBinder(@NonNull ServerContex return new INativeDisplayRootService.Stub() { @Override public IBinder waitForDisplayBinder(String serviceName) { - return doWaitForDisplayBinder(serviceName); + return doWaitForDisplayBinder(ctx, serviceName); } @Override @@ -96,32 +101,62 @@ private static IBinder smCall(@NonNull String method, @NonNull String name) { } } - private static IBinder waitForServiceWithTimeout(@NonNull String name, long timeoutMs) { - var holder = new IBinder[1]; - var t = new Thread(() -> holder[0] = smCall("waitForService", name), fmt("WaitSvc-%s", name)); - t.setDaemon(true); - t.start(); - try { - t.join(timeoutMs); - } catch (InterruptedException ignored) { + /** + * Look for [name] until it turns up or [WAIT_TOTAL_MS] passes. + * + * Polls {@code checkService} rather than calling {@code waitForService}, which blocks until + * the service appears and cannot be cancelled. A caller-side timeout around it does not stop + * it: the thread carrying it stays blocked for the life of the process, and while it waits, + * servicemanager's client logs a line a second, from every thread. For a VM that has stopped + * -- the display console left open after the VM exits, retrying -- the service never appears + * at all, so every attempt leaked another permanently-waiting, permanently-logging thread. + * Measured: 315884 log lines and 77 MB of daemon.log in four minutes, ending with the daemon + * dying and taking a different, running VM's crosvm down with it. + */ + private static IBinder pollForService(@NonNull String name) { + long deadline = System.nanoTime() + WAIT_TOTAL_MS * 1_000_000L; + while (System.nanoTime() < deadline) { + try { + Thread.sleep(WAIT_POLL_MS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return null; + } + var binder = smCall("checkService", name); + if (binder != null) return binder; } - return holder[0]; + return null; } - private static IBinder doWaitForDisplayBinder(@NonNull String serviceName) { - Log.i(TAG, fmt("waitForDisplayBinder('%s')", serviceName)); + /** + * Whether the VM behind [serviceName] is in a state where crosvm could still register it. + * + * A stopped VM will never register, so waiting for it is time the caller spends learning + * nothing -- and the caller is a console that retries. Unknown names are not ours to judge, + * so they wait as before. + */ + private static boolean vmCouldRegister(@NonNull ServerContext ctx, @NonNull String serviceName) { + if (inst == null) return true; + var state = inst.getState(); + return state == VMState.RUNNING || state == VMState.STARTING || state == VMState.REBOOTING; + } + + private static IBinder doWaitForDisplayBinder(@NonNull ServerContext ctx, + @NonNull String serviceName) { var direct = smCall("checkService", serviceName); - if (direct != null) { - Log.i(TAG, "OK: got display binder directly from ServiceManager"); - return direct; + if (direct != null) return direct; + if (!vmCouldRegister(ctx, serviceName)) { + Log.i(TAG, fmt("'%s': VM not running, nothing to wait for", serviceName)); + return null; } - Log.i(TAG, "Not found, waiting up to 5s..."); - var waited = waitForServiceWithTimeout(serviceName, 5000L); + Log.i(TAG, fmt("waitForDisplayBinder('%s'): not registered yet, looking for %d ms", + serviceName, WAIT_TOTAL_MS)); + var waited = pollForService(serviceName); if (waited != null) { - Log.i(TAG, "OK: got display binder via waitForService"); + Log.i(TAG, "OK: got display binder"); return waited; } - Log.e(TAG, fmt("'%s' not found - is crosvm running with " + Log.w(TAG, fmt("'%s' not found - is crosvm running with " + "--android-display-service %s?", serviceName, serviceName)); return null; } diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackendInstance.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackendInstance.java index 6f62a1819..7312bd6c3 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackendInstance.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/CrosvmBackendInstance.java @@ -105,9 +105,11 @@ public VMStartResult start() { // the VM is up - so the daemon is the only process that can both bind the socket before // crosvm starts and stay alive to feed it. We pre-bind + accept here; the UI forwards evdev // to us via the vm_input IPC command (see InputHandler). Server fds released on cleanup(). - if (isNativeDisplayEnabled()) { if (!inputBridge.startListening(NativeDisplay.serviceName(config))) { - Log.e(TAG, "Native display input sockets unavailable; crosvm will likely fail"); + // Single source of truth: isInputBridgeNeeded() gates both this pre-bind and the --input + if (isInputBridgeNeeded()) { + Log.e(TAG, "Display input sockets unavailable; crosvm will likely fail"); + } } } var args = buildCommand(); @@ -228,6 +230,12 @@ private List buildCommand() { buildSharedDirCommand(args); buildGpuCommand(args); buildVncCommand(args); + // The evdev --input devices ride along whenever any app display path is active: the native + // display routes everything through them; the VNC display routes its MOUSE (relative) and + // TOUCH (multi-touch) modes here while the tablet pointer + keyboard stay on RFB. + if (isInputBridgeNeeded()) { + buildInputDevicesCommand(args); + } buildSerialCommand(args); item.opt("extra_options", DataItem.newArray()) .forEach(arg -> args.add(arg.getValue().asString())); @@ -387,6 +395,7 @@ private void buildGpuCommand(@NonNull List args) { // socket pre-bind in start(), so the two must never diverge. if (isNativeDisplayEnabled()) { buildNativeDisplayCommand(args); + args.add("--android-display-service"); } } @@ -395,8 +404,6 @@ private void buildNativeDisplayCommand(@NonNull List args) { var serviceName = NativeDisplay.serviceName(config); var width = item.optLong("display_width", 1280); var height = item.optLong("display_height", 720); - args.add("--android-display-service"); - args.add(serviceName); // multi-touch ABS range must equal the guest resolution so view coords scale straight onto // ABS_X/ABS_Y (see EvdevEncoder / TouchScaleCalculator). args.add("--input"); @@ -404,11 +411,19 @@ private void buildNativeDisplayCommand(@NonNull List args) { "multi-touch[path=%s,width=%d,height=%d]", NativeDisplay.inputSocketPath(serviceName, NativeDisplay.MULTITOUCH), width, height )); + private void buildInputDevicesCommand(@NonNull List args) { + // Relative-pointer mouse (REL_X/Y + buttons + wheel) for InputMode.MOUSE; the guest renders + // the cursor, which is what relative-motion consumers (FPS games) need. args.add("--input"); args.add(fmt( "keyboard[path=%s]", NativeDisplay.inputSocketPath(serviceName, NativeDisplay.KEYBOARD) + "mouse[path=%s]", )); + )); + args.add("--input"); + args.add(fmt( + )); } /** True iff the per-VM crosvm command will reference native-display input sockets. */ @@ -424,6 +439,13 @@ private boolean isNativeDisplayEnabled() { private void buildVncCommand(@NonNull List args) { var item = config.item; if (!item.optBoolean("vnc_enabled", false)) return; + /** + * The evdev input bridge (and matching --input devices) is needed by both app display paths: + * native uses it for every input; the VNC display uses it for MOUSE/TOUCH modes (tablet + */ + private boolean isInputBridgeNeeded() { + } + var vncArg = new StringBuilder(); var host = item.optString("vnc_host", ""); if (!host.isEmpty()) { diff --git a/app/src/main/java/cn/classfun/droidvm/lib/perf/SystemGestureGuard.java b/app/src/main/java/cn/classfun/droidvm/lib/perf/SystemGestureGuard.java new file mode 100644 index 000000000..52db9bceb --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/lib/perf/SystemGestureGuard.java @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. +package cn.classfun.droidvm.lib.perf; + +import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; + +import android.util.Log; + +import androidx.annotation.NonNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import cn.classfun.droidvm.lib.run.RunContext; +import cn.classfun.droidvm.lib.run.RunResult; +import cn.classfun.droidvm.lib.run.root.RootRunContext; + +/** + * Suppresses OEM full-screen touch gestures while a VM display is in the foreground, so + * multi-finger input reaches the guest instead of the host system. + * + *

    Why this exists: ColorOS/OxygenOS intercepts three-finger touches globally -- swipe-down + * takes a screenshot, touch-and-hold starts a partial screenshot -- and unlike the navigation + * back gesture there is NO public per-app opt-out ({@code setSystemGestureExclusionRects} only + * covers screen-edge gestures, and the game-mode declaration in {@link GamePerfHint} does not + * suppress it either). A guest desktop, however, has its own three-finger gestures (pinch zoom, + * workspace switch), which the host eats before the guest ever sees a pointer event. + * + *

    So: while (and only while) the native display is foreground, the OEM toggles below are + * turned off through the root shell, and restored to their previous values on exit. On devices + * without these keys (`settings get` prints "null") this is a no-op, so calling it + * unconditionally on every device is safe. + * + *

    If the app process dies while the display is up, the exit path never runs and the user's + * gesture setting stays off until the next display session restores it on entry -- an accepted + * trade-off for not persisting state; the keys are re-read (not assumed) on every enter. + */ +public final class SystemGestureGuard { + private static final String TAG = "SystemGestureGuard"; + + /** OEM gesture toggles (system namespace) that swallow multi-finger touches. */ + private static final String[] KEYS = { + // ColorOS/OxygenOS "smart apperceive" screenshot: three-finger swipe & touch-and-hold. + "oplus_customize_smart_apperceive_screen_capture", + // Three-finger sideways swipe to switch apps. + "oplus_customize_three_fingers_switch_app", + }; + + /** Serializes enter/exit so a fast pause/resume cannot interleave get and put. */ + private static final ExecutorService executor = Executors.newSingleThreadExecutor(); + + /** Keys that were "1" on enter and must go back to "1" on exit. */ + private static final List suppressed = new ArrayList<>(); + + private SystemGestureGuard() { + } + + /** Turns the OEM gestures off; call when the VM display becomes foreground. */ + public static void enterDisplay() { + executor.execute(() -> { + RunContext shell = RootRunContext.getContext(); + synchronized (suppressed) { + // A re-enter without exit (activity recreation) must not re-read "0" as the + // value to restore, so the restore list only grows from a clean slate. + if (!suppressed.isEmpty()) return; + for (String key : KEYS) { + RunResult get = shell.runQuiet(fmt("settings get system %s", key)); + if (!get.isSuccess() || !"1".equals(get.getOutString())) continue; + if (shell.runQuiet(fmt("settings put system %s 0", key)).isSuccess()) { + suppressed.add(key); + Log.i(TAG, fmt("suppressed host gesture: %s", key)); + } + } + } + }); + } + + /** Restores whatever {@link #enterDisplay} turned off; call when the display leaves. */ + public static void exitDisplay() { + executor.execute(() -> { + RunContext shell = RootRunContext.getContext(); + synchronized (suppressed) { + for (String key : suppressed) { + shell.runQuiet(fmt("settings put system %s 1", key)); + Log.i(TAG, fmt("restored host gesture: %s", key)); + } + suppressed.clear(); + } + }); + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/NativeDisplay.java b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/NativeDisplay.java index 4e211b600..04be7e303 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/NativeDisplay.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/NativeDisplay.java @@ -19,10 +19,15 @@ * registers (--android-display-service) and the VM's input-socket set. */ public final class NativeDisplay { - /** Input channels (MVP: multi-touch + keyboard). */ + /** + * Input channels. Each maps to one crosvm {@code --input } virtio-input device and one + * channel ids shared with the daemon's InputHandler; append new channels, never renumber. + */ public static final int MULTITOUCH = 0; public static final int KEYBOARD = 1; - public static final int CHANNEL_COUNT = 2; + public static final int MOUSE = 2; + public static final int TABLET = 3; + public static final int CHANNEL_COUNT = 4; /** * Broadcast the daemon (running as root) sends to hand its INativeDisplayRootService binder to @@ -38,7 +43,6 @@ public final class NativeDisplay { /** Per-attach random token; the UI only accepts a broadcast carrying the nonce it requested. */ public static final String EXTRA_NONCE = "nonce"; - private static final String[] KINDS = {"multitouch", "keyboard"}; private static final String RUN_PATH = pathJoin(DATA_DIR, "run"); private NativeDisplay() { diff --git a/app/src/main/java/cn/classfun/droidvm/lib/ui/MaterialMenu.java b/app/src/main/java/cn/classfun/droidvm/lib/ui/MaterialMenu.java index c3b32e6fc..fc4e6e1bf 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/ui/MaterialMenu.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/ui/MaterialMenu.java @@ -42,6 +42,7 @@ public final class MaterialMenu { private final ListPopupWindow popup; private MenuItem.OnMenuItemClickListener listener; private int gravity = Gravity.NO_GRAVITY; + private View headerView; public MaterialMenu(@NonNull Context context, @NonNull View anchor) { this.context = context; @@ -74,6 +75,19 @@ public void setOnMenuItemClickListener( this.listener = listener; } + /** + * Custom view rendered above the menu list inside the popup (ListPopupWindow prompt view) - + * e.g. a segmented input-mode selector. Set before {@link #show()}. + */ + public void setHeaderView(@Nullable View header) { + this.headerView = header; + } + + /** Closes the popup; header-view controls use this after handling a selection. */ + public void dismiss() { + popup.dismiss(); + } + @SuppressWarnings("unused") public void setGravity(int gravity) { this.gravity = gravity; @@ -107,6 +121,15 @@ private int preparePopup() { var adapter = new MenuAdapter(context, items, hasIcons); popup.setAdapter(adapter); var size = measureContent(adapter); + if (headerView != null) { + // Rendered above the list; grow the popup to fit it. + int spec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); + headerView.measure(spec, spec); + size.x = Math.max(size.x, headerView.getMeasuredWidth()); + size.y += headerView.getMeasuredHeight(); + popup.setPromptPosition(ListPopupWindow.POSITION_PROMPT_ABOVE); + popup.setPromptView(headerView); + } popup.setContentWidth(size.x); popup.setHeight(size.y); popup.setBackgroundDrawable(createMaterial3Background()); diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/BaseExtraKeysAdapter.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/BaseExtraKeysAdapter.java index 295d48b2f..8a742154e 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/BaseExtraKeysAdapter.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/BaseExtraKeysAdapter.java @@ -21,6 +21,10 @@ public abstract class BaseExtraKeysAdapter implements KeyListener { protected final DisplayExtraKeysPanel panel; private boolean ctrlSticky, altSticky, shiftSticky, winSticky; + // Keys currently held via onKey(). One-shot modifiers wrap the whole hold group (applied on + // the first key down, released after the last key up) so simultaneous holds like W+A don't + // lose their modifiers halfway through. + private int heldKeys; protected BaseExtraKeysAdapter(@NonNull DisplayExtraKeysPanel panel) { this.panel = panel; @@ -57,13 +61,19 @@ public void applyModifiers(boolean down) { } } - /** Taps a key with the active modifiers wrapped around it. */ - protected void tapKey(int androidKeyCode) { - if (!isReady()) return; - applyModifiers(true); - emitKey(androidKeyCode, true); - emitKey(androidKeyCode, false); - applyModifiers(false); + @Override + public void onKey(int androidKeyCode, boolean down) { + if (!isReady()) { + heldKeys = 0; + return; + } + if (down) { + if (heldKeys++ == 0) applyModifiers(true); + emitKey(androidKeyCode, true); + } else { + emitKey(androidKeyCode, false); + if (heldKeys > 0 && --heldKeys == 0) applyModifiers(false); + } } @Override diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/DaemonDisplayAttach.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/DaemonDisplayAttach.java new file mode 100644 index 000000000..2d347c85b --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/DaemonDisplayAttach.java @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. +package cn.classfun.droidvm.ui.vm.display.base; + +import android.app.Activity; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Handler; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.UUID; + +import cn.classfun.droidvm.display.INativeDisplayRootService; +import cn.classfun.droidvm.lib.store.vm.NativeDisplay; + +/** + * Acquires the daemon's broker binder ({@link INativeDisplayRootService}) for a display activity. + * The binder can't ride the JSON-RPC channel, so the flow is: send {@code display_attach} with a + * per-attach random nonce, and the daemon (uid=0) broadcasts the binder back; only the broadcast + * carrying our nonce is accepted, so another app spoofing the exported action can't slip us a + * fake broker. Retries while the daemon connection is still coming up. Shared by the native and + * VNC display activities, which use the binder for the direct evdev input sink (and the native + * path additionally for the per-VM display binder lookup). + */ +public final class DaemonDisplayAttach { + private static final String TAG = "DaemonDisplayAttach"; + private static final int MAX_ATTEMPTS = 10; + private static final long RETRY_DELAY_MS = 500; + + public interface Listener { + /** The broker binder arrived (fired once, on the main thread). */ + void onAttached(@NonNull INativeDisplayRootService service); + + /** The daemon died after attach; direct paths must fall back (main thread). */ + void onLost(); + } + + private final Activity activity; + private final Handler mainHandler; + private final Listener listener; + private final String nonce = UUID.randomUUID().toString(); + @Nullable + private INativeDisplayRootService service; + private boolean receiverRegistered; + private boolean attached; + + private final BroadcastReceiver receiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + var bundle = intent.getBundleExtra(NativeDisplay.EXTRA_BUNDLE); + if (bundle == null || !nonce.equals(bundle.getString(NativeDisplay.EXTRA_NONCE))) { + return; + } + var binder = bundle.getBinder(NativeDisplay.EXTRA_BINDER); + if (binder != null) { + onBinderReceived(binder); + } + } + }; + + private final IBinder.DeathRecipient deathRecipient; + + public DaemonDisplayAttach(@NonNull Activity activity, @NonNull Handler mainHandler, + @NonNull Listener listener) { + this.activity = activity; + this.mainHandler = mainHandler; + this.listener = listener; + this.deathRecipient = () -> mainHandler.post(() -> { + Log.w(TAG, "daemon broker binder died"); + service = null; + listener.onLost(); + }); + } + + /** Registers the nonce-matched broadcast receiver and requests the broker binder. */ + public void start() { + activity.registerReceiver(receiver, + new IntentFilter(NativeDisplay.BINDER_BROADCAST_ACTION), Context.RECEIVER_EXPORTED); + receiverRegistered = true; + request(MAX_ATTEMPTS); + } + + /** The broker binder, or null before attach / after the daemon died. */ + @Nullable + public INativeDisplayRootService getService() { + return service; + } + + public void stop() { + if (receiverRegistered) { + try { + activity.unregisterReceiver(receiver); + } catch (Exception ignored) { + } + receiverRegistered = false; + } + if (service != null) { + try { + service.asBinder().unlinkToDeath(deathRecipient, 0); + } catch (Exception ignored) { + } + } + service = null; + } + + private void request(int attemptsLeft) { + if (attached || activity.isFinishing()) { + return; + } + cn.classfun.droidvm.lib.daemon.DaemonConnection.getInstance().buildRequest("display_attach") + .put("nonce", nonce) + .onError(e -> retry(attemptsLeft)) + .onUnsuccessful(r -> retry(attemptsLeft)) + .invoke(); + } + + private void retry(int attemptsLeft) { + if (attemptsLeft <= 0) { + Log.w(TAG, "display_attach exhausted retries; daemon broker unavailable"); + return; + } + mainHandler.postDelayed(() -> request(attemptsLeft - 1), RETRY_DELAY_MS); + } + + private void onBinderReceived(@NonNull IBinder binder) { + if (attached) { + return; // ignore duplicate broadcasts + } + attached = true; + var svc = INativeDisplayRootService.Stub.asInterface(binder); + service = svc; + try { + binder.linkToDeath(deathRecipient, 0); + } catch (RemoteException e) { + Log.w(TAG, "linkToDeath failed", e); + } + listener.onAttached(svc); + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/DisplayChromeController.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/DisplayChromeController.java new file mode 100644 index 000000000..46f3395c5 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/DisplayChromeController.java @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. +package cn.classfun.droidvm.ui.vm.display.base; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * Single source of truth for the display chrome: toolbar / status bar / system bars, and which + * parts of the on-screen keyboard are up. Everything that used to flip view visibility directly + * (fullscreen toggle, menu items, keyboard buttons) mutates this state instead, and the one + * {@link Host#applyChrome} callback writes the whole set atomically - so the pieces can never + * drift apart. + * + * Rules: + *

      + *
    • The keyboard mode picks the Main zone: nothing, the one-row companion to the system IME, + * or the laptop keyboard.
    • + *
    • The Extra zone is the point of the system-IME mode - the keys an IME cannot send - so + * it is always up there; only the laptop mode, whose Main block already carries most of + * them, lets it be toggled off. FNx is a toggle in both. Both keep their values through + * {@link KeyboardMode#NONE}: hiding the keyboard is not the same as turning its zones + * off.
    • + *
    • Fullscreen hides toolbar + status bar + system bars + the whole keyboard. The state + * before entering is remembered and restored on exit; changes made while fullscreen show + * on top of it and are not persisted.
    • + *
    • No auto-hide: bar visibility is a pure function of this state. Transient system bars + * swiped in during fullscreen are the system's overlay and don't touch it.
    • + *
    + */ +public final class DisplayChromeController { + public interface Host { + /** + * Write the whole chrome set: toolbar/status bar/system bars shown iff + * {@code !fullscreen}; the keyboard's Main zone per {@code mode}; the Extra and FNx + * zones shown iff their flag is set and {@code mode} shows a keyboard at all. The host + * should re-request insets after applying so the display area updates in one pass. + */ + void applyChrome( + boolean fullscreen, + @NonNull KeyboardMode mode, + boolean extraVisible, + boolean fnxVisible); + } + + /** Persistence hook: fired after a (non-fullscreen) user action changes the state. */ + public interface StateListener { + void onUserStateChanged( + @NonNull KeyboardMode mode, boolean extraVisible, boolean fnxVisible); + } + + private final Host host; + @Nullable + private StateListener stateListener; + private boolean fullscreen; + private KeyboardMode mode; + private boolean extraVisible; + private boolean fnxVisible; + // Pre-fullscreen memory. + private KeyboardMode savedMode; + private boolean savedExtra, savedFnx; + + public DisplayChromeController( + @NonNull KeyboardMode mode, + boolean extraVisible, + boolean fnxVisible, + @NonNull Host host + ) { + this.host = host; + this.mode = mode; + this.extraVisible = extraVisible; + this.fnxVisible = fnxVisible; + } + + public void setStateListener(@Nullable StateListener listener) { + this.stateListener = listener; + } + + /** Push the initial state to the host once its views are ready. */ + public void applyInitial() { + apply(); + } + + public void toggleFullscreen() { + setFullscreen(!fullscreen); + } + + public void setFullscreen(boolean enabled) { + if (fullscreen == enabled) { + return; + } + fullscreen = enabled; + if (enabled) { + savedMode = mode; + savedExtra = extraVisible; + savedFnx = fnxVisible; + mode = KeyboardMode.NONE; + } else { + mode = savedMode == null ? KeyboardMode.NONE : savedMode; + extraVisible = savedExtra; + fnxVisible = savedFnx; + } + apply(); + } + + public void setKeyboardMode(@NonNull KeyboardMode newMode) { + if (mode == newMode) return; + mode = newMode; + notifyState(); + apply(); + } + + public void toggleExtraZone() { + extraVisible = !extraVisible; + notifyState(); + apply(); + } + + public void toggleFnxZone() { + fnxVisible = !fnxVisible; + notifyState(); + apply(); + } + + public boolean isFullscreen() { + return fullscreen; + } + + @NonNull + public KeyboardMode getKeyboardMode() { + return mode; + } + + public boolean isExtraVisible() { + return extraVisible; + } + + public boolean isFnxVisible() { + return fnxVisible; + } + + private void notifyState() { + if (stateListener != null && !fullscreen) + stateListener.onUserStateChanged(mode, extraVisible, fnxVisible); + } + + private void apply() { + boolean showing = mode.showsKeyboard(); + boolean extra = mode == KeyboardMode.SYSTEM || (showing && extraVisible); + host.applyChrome(fullscreen, mode, extra, showing && fnxVisible); + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/DisplayExtraKeysPanel.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/DisplayExtraKeysPanel.java index 2152723e4..18ae8b526 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/DisplayExtraKeysPanel.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/DisplayExtraKeysPanel.java @@ -5,20 +5,15 @@ import static android.view.HapticFeedbackConstants.KEYBOARD_TAP; -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ValueAnimator; import android.annotation.SuppressLint; import android.content.Context; -import android.os.Handler; -import android.os.Looper; +import android.content.res.ColorStateList; import android.util.AttributeSet; import android.view.KeyEvent; import android.view.LayoutInflater; -import android.view.MotionEvent; import android.view.View; -import android.view.ViewGroup; import android.widget.Button; +import android.widget.ImageButton; import android.widget.LinearLayout; import androidx.annotation.NonNull; @@ -26,20 +21,43 @@ import cn.classfun.droidvm.R; +/** + * The keyboard panel's shared zones: Extra (nav cluster and the keys no soft keyboard sends), + * FNx (function keys, shared by both keyboard modes), and the Main row used in system-IME mode. + * The laptop keyboard is {@link DisplayPhysicalKeyboardView}, docked below this and supplying + * its own Main - so whichever mode is up, Extra and FNx look and behave identically. + * + * Non-modifier keys send real key down/up ({@link HoldKeyGroup}: press = down, sliding onto + * another key presses that one, release = up), so the guest sees holds and does its own + * auto-repeat. Shift/Ctrl/Alt/Win are sticky - tap for one-shot, long-press to lock - with + * {@link BaseExtraKeysAdapter} owning that state and this panel rendering it. + */ public final class DisplayExtraKeysPanel extends LinearLayout { - private static final long ANIM_DURATION = 200; - private static final long KEY_REPEAT_DELAY_MS = 400; - private static final long KEY_REPEAT_INTERVAL_MS = 50; - private final Handler handler = new Handler(Looper.getMainLooper()); - private Runnable activeRepeatRunnable; - private LinearLayout fnKeysContainer; - private boolean capsActive = false; - private boolean fnxActive = false; + /** Zone toggle and IME summon live on the keys themselves, not in a menu. */ + public interface ZoneListener { + void onToggleFnxZone(); + + void onShowSystemKeyboard(); + } + + private View extraZone; + private View fnxZone; + private View systemMainRow; @Nullable private KeyListener keyListener; + @Nullable + private ZoneListener zoneListener; + // Notified whenever the modifier toggle state repaints, so a second view of the same state + // (the laptop keyboard's modifier keys) can repaint too. + @Nullable + private Runnable modifierStateObserver; private boolean ctrlDown, altDown, shiftDown, winDown; + private final HoldKeyGroup holdKeys = new HoldKeyGroup((code, down) -> { + if (keyListener != null) keyListener.onKey(code, down); + }); + public DisplayExtraKeysPanel(@NonNull Context context) { super(context); init(context); @@ -50,7 +68,8 @@ public DisplayExtraKeysPanel(@NonNull Context context, @Nullable AttributeSet at init(context); } - public DisplayExtraKeysPanel(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + public DisplayExtraKeysPanel( + @NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context); } @@ -58,7 +77,9 @@ public DisplayExtraKeysPanel(@NonNull Context context, @Nullable AttributeSet at private void init(@NonNull Context context) { setOrientation(VERTICAL); LayoutInflater.from(context).inflate(R.layout.widget_display_extra_keys, this, true); - fnKeysContainer = findViewById(R.id.fn_keys_container); + extraZone = findViewById(R.id.extra_zone); + fnxZone = findViewById(R.id.fnx_zone); + systemMainRow = findViewById(R.id.system_main_row); setupKeys(); } @@ -66,31 +87,30 @@ public void setKeyListener(@Nullable KeyListener listener) { this.keyListener = listener; } - @SuppressWarnings("unused") + public void setZoneListener(@Nullable ZoneListener listener) { + this.zoneListener = listener; + } + + public void setModifierStateObserver(@Nullable Runnable observer) { + this.modifierStateObserver = observer; + } + public boolean isCtrlDown() { return ctrlDown; } - @SuppressWarnings("unused") public boolean isAltDown() { return altDown; } - @SuppressWarnings("unused") public boolean isShiftDown() { return shiftDown; } - @SuppressWarnings("unused") public boolean isWinDown() { return winDown; } - @SuppressWarnings("unused") - public boolean isCapsActive() { - return capsActive; - } - public void setCtrlDown(boolean v) { ctrlDown = v; updateToggleButtons(); @@ -116,107 +136,94 @@ public void updateToggleButtons() { setToggleStyle(findViewById(R.id.btn_alt), altDown); setToggleStyle(findViewById(R.id.btn_shift), shiftDown); setToggleStyle(findViewById(R.id.btn_win), winDown); - setToggleStyle(findViewById(R.id.btn_cap), capsActive); - setToggleStyle(findViewById(R.id.btn_fnx), fnxActive); + if (modifierStateObserver != null) modifierStateObserver.run(); } - private void setToggleStyle(@Nullable Button btn, boolean active) { - if (btn == null) return; - if (active) { - btn.setBackgroundColor(getContext().getColor(R.color.extra_key_bg_active)); - btn.setTextColor(getContext().getColor(R.color.extra_key_text_active)); - } else { - btn.setBackground(null); - btn.setTextColor(getContext().getColor(R.color.extra_key_text)); - } + /** Repaint the Fn toggle to match the zone actually on screen. */ + public void setZoneToggleState(boolean fnxOn) { + setToggleStyle(findViewById(R.id.btn_zone_fnx), fnxOn); + } + + /** + * Which key owns the Main row's last slot. The IME's own visibility decides: while it is up + * the slot toggles the Fn zone, and while it is dismissed it summons the IME back - the + * other key would do nothing in each case. The Fn zone itself keeps its state throughout; + * only the way to toggle it goes away with the IME. + */ + public void setImeVisible(boolean imeVisible) { + findViewById(R.id.btn_zone_fnx).setVisibility(imeVisible ? VISIBLE : GONE); + findViewById(R.id.btn_show_ime).setVisibility(imeVisible ? GONE : VISIBLE); + } + + /** Works for both kinds of key: a text {@link Button} and an icon-only {@link ImageButton}. */ + private void setToggleStyle(@Nullable View key, boolean active) { + if (key == null) return; + int color = getContext().getColor( + active ? R.color.extra_key_text_active : R.color.extra_key_text); + if (active) key.setBackgroundColor(getContext().getColor(R.color.extra_key_bg_active)); + else key.setBackgroundResource(R.drawable.extra_key_bg); + if (key instanceof Button) ((Button) key).setTextColor(color); + else if (key instanceof ImageButton) + ((ImageButton) key).setImageTintList(ColorStateList.valueOf(color)); } private void setupKeys() { - setKeyRepeat(R.id.btn_esc, () -> fireKeyRepeat(KeyEvent.KEYCODE_ESCAPE)); - setKeyRepeat(R.id.btn_slash, () -> fireCharRepeat('/')); - setKeyRepeat(R.id.btn_dash, () -> fireCharRepeat('-')); - setKeyRepeat(R.id.btn_home, () -> fireKeyRepeat(KeyEvent.KEYCODE_MOVE_HOME)); - setKeyRepeat(R.id.btn_up, () -> fireKeyRepeat(KeyEvent.KEYCODE_DPAD_UP)); - setKeyRepeat(R.id.btn_end, () -> fireKeyRepeat(KeyEvent.KEYCODE_MOVE_END)); - setKeyRepeat(R.id.btn_pgup, () -> fireKeyRepeat(KeyEvent.KEYCODE_PAGE_UP)); - setKeyRepeat(R.id.btn_tab, () -> fireKeyRepeat(KeyEvent.KEYCODE_TAB)); + setupHoldKey(R.id.btn_esc, KeyEvent.KEYCODE_ESCAPE); + setupHoldKey(R.id.btn_slash, KeyEvent.KEYCODE_SLASH); + setupHoldKey(R.id.btn_dash, KeyEvent.KEYCODE_MINUS); + setupHoldKey(R.id.btn_home, KeyEvent.KEYCODE_MOVE_HOME); + setupHoldKey(R.id.btn_up, KeyEvent.KEYCODE_DPAD_UP); + setupHoldKey(R.id.btn_end, KeyEvent.KEYCODE_MOVE_END); + setupHoldKey(R.id.btn_pgup, KeyEvent.KEYCODE_PAGE_UP); + setupHoldKey(R.id.btn_bksp, KeyEvent.KEYCODE_DEL); + setupHoldKey(R.id.btn_del, KeyEvent.KEYCODE_FORWARD_DEL); + setupHoldKey(R.id.btn_ins, KeyEvent.KEYCODE_INSERT); + setupHoldKey(R.id.btn_left, KeyEvent.KEYCODE_DPAD_LEFT); + setupHoldKey(R.id.btn_down, KeyEvent.KEYCODE_DPAD_DOWN); + setupHoldKey(R.id.btn_right, KeyEvent.KEYCODE_DPAD_RIGHT); + setupHoldKey(R.id.btn_pgdn, KeyEvent.KEYCODE_PAGE_DOWN); + setupHoldKey(R.id.btn_f1, KeyEvent.KEYCODE_F1); + setupHoldKey(R.id.btn_f2, KeyEvent.KEYCODE_F2); + setupHoldKey(R.id.btn_f3, KeyEvent.KEYCODE_F3); + setupHoldKey(R.id.btn_f4, KeyEvent.KEYCODE_F4); + setupHoldKey(R.id.btn_f5, KeyEvent.KEYCODE_F5); + setupHoldKey(R.id.btn_f6, KeyEvent.KEYCODE_F6); + setupHoldKey(R.id.btn_prtsc, KeyEvent.KEYCODE_SYSRQ); + setupHoldKey(R.id.btn_f7, KeyEvent.KEYCODE_F7); + setupHoldKey(R.id.btn_f8, KeyEvent.KEYCODE_F8); + setupHoldKey(R.id.btn_f9, KeyEvent.KEYCODE_F9); + setupHoldKey(R.id.btn_f10, KeyEvent.KEYCODE_F10); + setupHoldKey(R.id.btn_f11, KeyEvent.KEYCODE_F11); + setupHoldKey(R.id.btn_f12, KeyEvent.KEYCODE_F12); + setupHoldKey(R.id.btn_pause, KeyEvent.KEYCODE_BREAK); + setupHoldKey(R.id.btn_tab, KeyEvent.KEYCODE_TAB); + setupHoldKey(R.id.btn_enter, KeyEvent.KEYCODE_ENTER); setupModifierKey(R.id.btn_ctrl, KeyEvent.KEYCODE_CTRL_LEFT); - setupModifierKey(R.id.btn_alt, KeyEvent.KEYCODE_ALT_LEFT); - setKeyRepeat(R.id.btn_left, () -> fireKeyRepeat(KeyEvent.KEYCODE_DPAD_LEFT)); - setKeyRepeat(R.id.btn_down, () -> fireKeyRepeat(KeyEvent.KEYCODE_DPAD_DOWN)); - setKeyRepeat(R.id.btn_right, () -> fireKeyRepeat(KeyEvent.KEYCODE_DPAD_RIGHT)); - setKeyRepeat(R.id.btn_pgdn, () -> fireKeyRepeat(KeyEvent.KEYCODE_PAGE_DOWN)); + setupModifierKey(R.id.btn_shift, KeyEvent.KEYCODE_SHIFT_LEFT); setupModifierKey(R.id.btn_win, KeyEvent.KEYCODE_META_LEFT); - setKeyClick(R.id.btn_cap, v -> { - capsActive = !capsActive; - if (keyListener != null) keyListener.onCapsToggle(capsActive); - updateToggleButtons(); + setupModifierKey(R.id.btn_alt, KeyEvent.KEYCODE_ALT_LEFT); + setupTapKey(R.id.btn_zone_fnx, () -> { + if (zoneListener != null) zoneListener.onToggleFnxZone(); }); - setupModifierKey(R.id.btn_shift, KeyEvent.KEYCODE_SHIFT_LEFT); - setKeyRepeat(R.id.btn_del, () -> fireKeyRepeat(KeyEvent.KEYCODE_FORWARD_DEL)); - setKeyRepeat(R.id.btn_ins, () -> fireKeyRepeat(KeyEvent.KEYCODE_INSERT)); - setKeyRepeat(R.id.btn_enter, () -> fireKeyRepeat(KeyEvent.KEYCODE_ENTER)); - setKeyClick(R.id.btn_fnx, v -> { - fnxActive = !fnxActive; - setRowVisible(fnKeysContainer, fnxActive); - updateToggleButtons(); + setupTapKey(R.id.btn_show_ime, () -> { + if (zoneListener != null) zoneListener.onShowSystemKeyboard(); }); - setKeyRepeat(R.id.btn_f1, () -> fireKeyRepeat(KeyEvent.KEYCODE_F1)); - setKeyRepeat(R.id.btn_f2, () -> fireKeyRepeat(KeyEvent.KEYCODE_F2)); - setKeyRepeat(R.id.btn_f3, () -> fireKeyRepeat(KeyEvent.KEYCODE_F3)); - setKeyRepeat(R.id.btn_f4, () -> fireKeyRepeat(KeyEvent.KEYCODE_F4)); - setKeyRepeat(R.id.btn_f5, () -> fireKeyRepeat(KeyEvent.KEYCODE_F5)); - setKeyRepeat(R.id.btn_f6, () -> fireKeyRepeat(KeyEvent.KEYCODE_F6)); - setKeyRepeat(R.id.btn_f7, () -> fireKeyRepeat(KeyEvent.KEYCODE_F7)); - setKeyRepeat(R.id.btn_f8, () -> fireKeyRepeat(KeyEvent.KEYCODE_F8)); - setKeyRepeat(R.id.btn_f9, () -> fireKeyRepeat(KeyEvent.KEYCODE_F9)); - setKeyRepeat(R.id.btn_f10, () -> fireKeyRepeat(KeyEvent.KEYCODE_F10)); - setKeyRepeat(R.id.btn_f11, () -> fireKeyRepeat(KeyEvent.KEYCODE_F11)); - setKeyRepeat(R.id.btn_f12, () -> fireKeyRepeat(KeyEvent.KEYCODE_F12)); } - private void fireKeyRepeat(int keyCode) { - if (keyListener != null) keyListener.onKeyRepeat(keyCode); + private void setupHoldKey(int id, int keyCode) { + holdKeys.register(findViewById(id), keyCode); } - private void fireCharRepeat(char ch) { - if (keyListener != null) keyListener.onCharRepeat(ch); - } - - private void setKeyClick(int id, OnClickListener listener) { + private void setupTapKey(int id, @NonNull Runnable action) { findViewById(id).setOnClickListener(v -> { v.performHapticFeedback(KEYBOARD_TAP); - listener.onClick(v); + action.run(); }); } @SuppressLint("ClickableViewAccessibility") - private void setKeyRepeat(int id, Runnable action) { - findViewById(id).setOnTouchListener((v, event) -> { - switch (event.getActionMasked()) { - case MotionEvent.ACTION_DOWN: - v.setPressed(true); - v.performHapticFeedback(KEYBOARD_TAP); - action.run(); - stopKeyRepeat(); - activeRepeatRunnable = () -> { - action.run(); - handler.postDelayed(activeRepeatRunnable, KEY_REPEAT_INTERVAL_MS); - }; - handler.postDelayed(activeRepeatRunnable, KEY_REPEAT_DELAY_MS); - return true; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - v.setPressed(false); - stopKeyRepeat(); - return true; - } - return false; - }); - } - private void setupModifierKey(int btnId, int keyCode) { - View btn = findViewById(btnId); + var btn = findViewById(btnId); btn.setOnClickListener(v -> { v.performHapticFeedback(KEYBOARD_TAP); if (keyListener != null) keyListener.onModifierClick(keyCode); @@ -228,77 +235,30 @@ private void setupModifierKey(int btnId, int keyCode) { }); } - public void stopKeyRepeat() { - if (activeRepeatRunnable != null) { - handler.removeCallbacks(activeRepeatRunnable); - activeRepeatRunnable = null; + /** + * Show the zones the chrome state calls for. The panel itself is visible whenever any zone + * is - an empty panel would otherwise leave a blank strip over the display. + */ + public void applyZones(boolean extraOn, boolean fnxOn, boolean systemMainOn) { + setZoneToggleState(fnxOn); + boolean any = extraOn || fnxOn || systemMainOn; + if (!any) { + // Slide the whole panel away; the zones keep their own states for the way back. + ViewHeightAnimator.hide(this); + return; } - } - - public void animateIn() { - animateRowIn(this); - } - - public void animateOut() { - animateRowOut(this); - } - - public void setVisibleAnimated(boolean visible) { - if (visible) animateIn(); - else animateOut(); - } - - private void animateRowIn(@NonNull View row) { - if (row.getVisibility() == VISIBLE) return; - row.setVisibility(VISIBLE); - row.measure( - MeasureSpec.makeMeasureSpec( - ((View) row.getParent()).getWidth(), MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED) - ); - int target = row.getMeasuredHeight(); - var lp = row.getLayoutParams(); - lp.height = 0; - row.requestLayout(); - var anim = ValueAnimator.ofInt(0, target); - anim.setDuration(ANIM_DURATION); - anim.addUpdateListener(a -> { - lp.height = (int) a.getAnimatedValue(); - row.requestLayout(); - }); - anim.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator a) { - lp.height = ViewGroup.LayoutParams.WRAP_CONTENT; - row.requestLayout(); - } - }); - anim.start(); - } - - private void animateRowOut(@NonNull View row) { - if (row.getVisibility() == GONE) return; - int start = row.getHeight(); - var lp = row.getLayoutParams(); - var anim = ValueAnimator.ofInt(start, 0); - anim.setDuration(ANIM_DURATION); - anim.addUpdateListener(a -> { - lp.height = (int) a.getAnimatedValue(); - row.requestLayout(); - }); - anim.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator a) { - row.setVisibility(GONE); - lp.height = ViewGroup.LayoutParams.WRAP_CONTENT; - row.requestLayout(); - } - }); - anim.start(); - } - - private void setRowVisible(@NonNull View row, boolean visible) { - if (visible) animateRowIn(row); - else animateRowOut(row); + if (getVisibility() != VISIBLE) { + // Panel arriving: put the zones in place first, then animate the panel as a whole, + // so its slide-in is one movement rather than several nested ones. + extraZone.setVisibility(extraOn ? VISIBLE : GONE); + fnxZone.setVisibility(fnxOn ? VISIBLE : GONE); + systemMainRow.setVisibility(systemMainOn ? VISIBLE : GONE); + ViewHeightAnimator.show(this); + return; + } + // Panel already up: animate each zone so Ex/FNx slide open and shut. + ViewHeightAnimator.setVisible(extraZone, extraOn); + ViewHeightAnimator.setVisible(fnxZone, fnxOn); + ViewHeightAnimator.setVisible(systemMainRow, systemMainOn); } } diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/DisplayInputBackend.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/DisplayInputBackend.java new file mode 100644 index 000000000..3b7d38eb3 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/DisplayInputBackend.java @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. +package cn.classfun.droidvm.ui.vm.display.base; + +import androidx.annotation.NonNull; + +/** + * The seam between the shared display control bar (system keyboard, extra keys, input-mode selector, + * fullscreen, rotate) and a concrete display backend. Both backends implement this so the control + * bar is backend-agnostic: + * + *
      + * scan codes, pointer to the mode-appropriate virtio-input device. + *
    + * + * Only event delivery lives here. Fullscreen and screen rotation are window-level concerns the + * control bar drives on the host Activity directly, not through the backend. + */ +public interface DisplayInputBackend { + /** Sends a key by Android key code; the backend translates to its wire representation. */ + void sendKey(int androidKeyCode, boolean down); + + /** + * Sends a printable character, synthesizing whatever the guest needs for it (e.g. Shift-wrapping + * uppercase and shifted symbols on the evdev backend). Used by the soft keyboard's commit path. + */ + void sendChar(char c); + + /** + * The pointer input mode changed. The backend adjusts how it turns subsequent on-screen touches + * into guest input (multi-touch, relative mouse, or absolute single-pointer tablet). Backends + * that route to distinct guest input devices switch the target device here. + */ + void setInputMode(@NonNull InputMode mode); +} diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/DisplayKeyboardMenuRow.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/DisplayKeyboardMenuRow.java new file mode 100644 index 000000000..a25b953dd --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/DisplayKeyboardMenuRow.java @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. +package cn.classfun.droidvm.ui.vm.display.base; + +import android.view.LayoutInflater; +import android.view.View; + +import androidx.annotation.NonNull; + +import com.google.android.material.button.MaterialButtonToggleGroup; + +import java.util.function.Consumer; + +import cn.classfun.droidvm.R; + +/** + * The keyboard row of the display fab-menu header: pick the typing surface - none, the system + * IME plus its companion row, or the laptop keyboard. Only the surface is chosen here; the + * Extra and FNx zones are toggled from keys on the keyboard itself, where they are visible. + */ +public final class DisplayKeyboardMenuRow { + private DisplayKeyboardMenuRow() { + } + + @NonNull + public static View build( + @NonNull LayoutInflater inflater, + @NonNull KeyboardMode current, + @NonNull Consumer onPick, + @NonNull Runnable dismiss + ) { + var group = (MaterialButtonToggleGroup) + inflater.inflate(R.layout.view_keyboard_mode_toggle, null); + group.check(buttonFor(current)); + group.addOnButtonCheckedListener((g, checkedId, isChecked) -> { + if (!isChecked) return; + onPick.accept(modeFor(checkedId)); + dismiss.run(); + }); + return group; + } + + private static int buttonFor(@NonNull KeyboardMode mode) { + switch (mode) { + case NONE: + return R.id.mode_kb_none; + case LAPTOP: + return R.id.mode_kb_laptop; + default: + return R.id.mode_kb_system; + } + } + + @NonNull + private static KeyboardMode modeFor(int buttonId) { + if (buttonId == R.id.mode_kb_none) return KeyboardMode.NONE; + if (buttonId == R.id.mode_kb_laptop) return KeyboardMode.LAPTOP; + return KeyboardMode.SYSTEM; + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/DisplayPhysicalKeyboardView.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/DisplayPhysicalKeyboardView.java new file mode 100644 index 000000000..205b54542 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/DisplayPhysicalKeyboardView.java @@ -0,0 +1,330 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. +package cn.classfun.droidvm.ui.vm.display.base; + +import static android.view.HapticFeedbackConstants.KEYBOARD_TAP; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.KeyEvent; +import android.view.View; +import android.widget.Button; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.LinearLayout; + +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import cn.classfun.droidvm.R; + +/** + * The laptop keyboard: the Main zone of {@link KeyboardMode#LAPTOP}. Only the ANSI main block + * lives here - the function keys are the panel's shared FNx zone and the navigation cluster its + * Extra zone, so both modes show the same rows above whichever Main is up. The bottom row ends + * with the zone toggles and a button that puts the whole keyboard away. + * + * Non-modifier keys send real key down/up ({@link HoldKeyGroup}: press = down, sliding onto + * another key presses that one, release = up), so holds (WASD movement, held arrows) reach the + * guest and several keys can be held at once. Shift/Ctrl/Alt/Win are sticky, sharing their state + * with the panel through the adapter; {@link #refreshModifiers} repaints it here. + */ +public final class DisplayPhysicalKeyboardView extends LinearLayout { + /** The keys that act on the keyboard itself rather than sending anything to the guest. */ + public interface ZoneListener { + void onToggleExtraZone(); + + void onToggleFnxZone(); + + void onCloseKeyboard(); + } + + private static final int ROW_HEIGHT_DP = 40; + private static final float TEXT_SIZE_SP = 10f; + + private static final int KIND_NORMAL = 0; + private static final int KIND_MODIFIER = 1; + private static final int KIND_ZONE_EXTRA = 2; + private static final int KIND_ZONE_FNX = 3; + private static final int KIND_CLOSE = 4; + + private static final class Key { + final String label; + /** What the key reads while Shift is held; same as {@link #label} when it doesn't shift. */ + final String shiftLabel; + final int code; + final float weight; + final int kind; + final @DrawableRes int icon; + + Key(String label, String shiftLabel, int code, float weight, int kind, + @DrawableRes int icon) { + this.label = label; + this.shiftLabel = shiftLabel; + this.code = code; + this.weight = weight; + this.kind = kind; + this.icon = icon; + } + } + + /** Letter: lower case unshifted, upper case shifted, like the key it stands for. */ + private static Key a(String lower, int code) { + return new Key(lower, lower.toUpperCase(Locale.ROOT), code, 1f, KIND_NORMAL, 0); + } + + /** Key whose face changes under Shift (number row, punctuation). */ + private static Key k(String label, String shiftLabel, int code) { + return k(label, shiftLabel, code, 1f); + } + + private static Key k(String label, String shiftLabel, int code, float weight) { + return new Key(label, shiftLabel, code, weight, KIND_NORMAL, 0); + } + + private static Key k(String label, int code, float weight) { + return new Key(label, label, code, weight, KIND_NORMAL, 0); + } + + /** Key drawn as an icon; glyphs like tab and return come out thin and tiny in the font. */ + private static Key kIcon(@DrawableRes int icon, int code, float weight) { + return new Key("", "", code, weight, KIND_NORMAL, icon); + } + + private static Key m(String label, int code, float weight) { + return new Key(label, label, code, weight, KIND_MODIFIER, 0); + } + + private static Key mIcon(@DrawableRes int icon, int code, float weight) { + return new Key("", "", code, weight, KIND_MODIFIER, icon); + } + + private static Key special(String label, int kind, float weight, @DrawableRes int icon) { + return new Key(label, label, 0, weight, kind, icon); + } + + // Every row totals 15 weight units, so the columns line up down the block. + private static final Key[][] ROWS = { + { + k("`", "~", KeyEvent.KEYCODE_GRAVE), + k("1", "!", KeyEvent.KEYCODE_1), k("2", "@", KeyEvent.KEYCODE_2), + k("3", "#", KeyEvent.KEYCODE_3), k("4", "$", KeyEvent.KEYCODE_4), + k("5", "%", KeyEvent.KEYCODE_5), k("6", "^", KeyEvent.KEYCODE_6), + k("7", "&", KeyEvent.KEYCODE_7), k("8", "*", KeyEvent.KEYCODE_8), + k("9", "(", KeyEvent.KEYCODE_9), k("0", ")", KeyEvent.KEYCODE_0), + k("-", "_", KeyEvent.KEYCODE_MINUS), k("=", "+", KeyEvent.KEYCODE_EQUALS), + kIcon(R.drawable.ic_key_backspace, KeyEvent.KEYCODE_DEL, 2f), + }, + { + kIcon(R.drawable.ic_key_tab, KeyEvent.KEYCODE_TAB, 1.5f), + a("q", KeyEvent.KEYCODE_Q), a("w", KeyEvent.KEYCODE_W), + a("e", KeyEvent.KEYCODE_E), a("r", KeyEvent.KEYCODE_R), + a("t", KeyEvent.KEYCODE_T), a("y", KeyEvent.KEYCODE_Y), + a("u", KeyEvent.KEYCODE_U), a("i", KeyEvent.KEYCODE_I), + a("o", KeyEvent.KEYCODE_O), a("p", KeyEvent.KEYCODE_P), + k("[", "{", KeyEvent.KEYCODE_LEFT_BRACKET), + k("]", "}", KeyEvent.KEYCODE_RIGHT_BRACKET), + k("\\", "|", KeyEvent.KEYCODE_BACKSLASH, 1.5f), + }, + { + k("CAPS", KeyEvent.KEYCODE_CAPS_LOCK, 1.75f), + a("a", KeyEvent.KEYCODE_A), a("s", KeyEvent.KEYCODE_S), + a("d", KeyEvent.KEYCODE_D), a("f", KeyEvent.KEYCODE_F), + a("g", KeyEvent.KEYCODE_G), a("h", KeyEvent.KEYCODE_H), + a("j", KeyEvent.KEYCODE_J), a("k", KeyEvent.KEYCODE_K), + a("l", KeyEvent.KEYCODE_L), + k(";", ":", KeyEvent.KEYCODE_SEMICOLON), + k("'", "\"", KeyEvent.KEYCODE_APOSTROPHE), + kIcon(R.drawable.ic_key_enter, KeyEvent.KEYCODE_ENTER, 2.25f), + }, + { + mIcon(R.drawable.ic_key_shift, KeyEvent.KEYCODE_SHIFT_LEFT, 2.25f), + a("z", KeyEvent.KEYCODE_Z), a("x", KeyEvent.KEYCODE_X), + a("c", KeyEvent.KEYCODE_C), a("v", KeyEvent.KEYCODE_V), + a("b", KeyEvent.KEYCODE_B), a("n", KeyEvent.KEYCODE_N), + a("m", KeyEvent.KEYCODE_M), + k(",", "<", KeyEvent.KEYCODE_COMMA), k(".", ">", KeyEvent.KEYCODE_PERIOD), + k("/", "?", KeyEvent.KEYCODE_SLASH), + mIcon(R.drawable.ic_key_shift, KeyEvent.KEYCODE_SHIFT_LEFT, 2.75f), + }, + { + m("CTRL", KeyEvent.KEYCODE_CTRL_LEFT, 1.5f), + mIcon(R.drawable.ic_key_windows, KeyEvent.KEYCODE_META_LEFT, 1.5f), + m("ALT", KeyEvent.KEYCODE_ALT_LEFT, 1.5f), + kIcon(R.drawable.ic_key_space, KeyEvent.KEYCODE_SPACE, 4.5f), + m("ALT", KeyEvent.KEYCODE_ALT_LEFT, 1.5f), + m("CTRL", KeyEvent.KEYCODE_CTRL_LEFT, 1.5f), + special("Ex", KIND_ZONE_EXTRA, 1f, 0), + special("Fn", KIND_ZONE_FNX, 1f, 0), + special("", KIND_CLOSE, 1f, R.drawable.ic_keyboard_close), + }, + }; + + @Nullable + private KeyListener keyListener; + @Nullable + private ZoneListener zoneListener; + private final Map> modifierButtons = new HashMap<>(); + private final List extraZoneButtons = new ArrayList<>(); + private final List fnxZoneButtons = new ArrayList<>(); + // Keys whose face changes under Shift, so the keyboard reads like what it will type. + private final Map shiftableKeys = new HashMap<>(); + private final HoldKeyGroup holdKeys = new HoldKeyGroup((code, down) -> { + if (keyListener != null) keyListener.onKey(code, down); + }); + + public DisplayPhysicalKeyboardView(@NonNull Context context) { + super(context); + init(context); + } + + public DisplayPhysicalKeyboardView(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + init(context); + } + + public DisplayPhysicalKeyboardView( + @NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context); + } + + private void init(@NonNull Context context) { + setOrientation(VERTICAL); + for (Key[] rowSpec : ROWS) addView(buildRow(context, rowSpec)); + } + + @NonNull + private LinearLayout buildRow(@NonNull Context context, @NonNull Key[] rowSpec) { + int rowHeight = Math.round( + ROW_HEIGHT_DP * context.getResources().getDisplayMetrics().density); + var row = new LinearLayout(context); + row.setOrientation(HORIZONTAL); + row.setBackgroundColor(context.getColor(R.color.extra_keys_background)); + for (Key key : rowSpec) row.addView(buildKey(context, key)); + row.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, rowHeight)); + return row; + } + + @NonNull + private View buildKey(@NonNull Context context, @NonNull Key key) { + View view; + if (key.icon != 0) { + // An ImageButton centres its drawable; a Button's compound drawable does not, and at + // these key sizes the offset is obvious. + var img = new ImageButton(context); + img.setImageResource(key.icon); + img.setScaleType(ImageView.ScaleType.CENTER); + img.setBackgroundResource(R.drawable.extra_key_bg); + img.setImageTintList(ColorStateList.valueOf( + context.getColor(R.color.extra_key_text))); + img.setPadding(0, 0, 0, 0); + view = img; + } else { + // Same visual as the extra-keys strip; denser text since these rows fit up to 14 keys. + var btn = new Button(context, null, 0, R.style.ExtraKey); + btn.setText(key.label); + btn.setTextSize(TypedValue.COMPLEX_UNIT_SP, TEXT_SIZE_SP); + if (!key.label.equals(key.shiftLabel)) shiftableKeys.put(btn, key); + view = btn; + } + view.setLayoutParams(new LayoutParams(0, LayoutParams.MATCH_PARENT, key.weight)); + switch (key.kind) { + case KIND_MODIFIER: + modifierButtons.computeIfAbsent(key.code, c -> new ArrayList<>()).add(view); + view.setOnClickListener(v -> { + v.performHapticFeedback(KEYBOARD_TAP); + if (keyListener != null) keyListener.onModifierClick(key.code); + }); + view.setOnLongClickListener(v -> { + v.performHapticFeedback(KEYBOARD_TAP); + if (keyListener != null) keyListener.onModifierLongClick(key.code); + return true; + }); + break; + case KIND_ZONE_EXTRA: + extraZoneButtons.add(view); + setTapAction(view, () -> { + if (zoneListener != null) zoneListener.onToggleExtraZone(); + }); + break; + case KIND_ZONE_FNX: + fnxZoneButtons.add(view); + setTapAction(view, () -> { + if (zoneListener != null) zoneListener.onToggleFnxZone(); + }); + break; + case KIND_CLOSE: + setTapAction(view, () -> { + if (zoneListener != null) zoneListener.onCloseKeyboard(); + }); + break; + default: + holdKeys.register(view, key.code); + break; + } + return view; + } + + private void setTapAction(@NonNull View btn, @NonNull Runnable action) { + btn.setOnClickListener(v -> { + v.performHapticFeedback(KEYBOARD_TAP); + action.run(); + }); + } + + public void setKeyListener(@Nullable KeyListener listener) { + this.keyListener = listener; + } + + public void setZoneListener(@Nullable ZoneListener listener) { + this.zoneListener = listener; + } + + /** Repaint the sticky-modifier keys, and the key faces Shift changes, from adapter state. */ + public void refreshModifiers(boolean ctrl, boolean alt, boolean shift, boolean win) { + paintModifier(KeyEvent.KEYCODE_CTRL_LEFT, ctrl); + paintModifier(KeyEvent.KEYCODE_ALT_LEFT, alt); + paintModifier(KeyEvent.KEYCODE_SHIFT_LEFT, shift); + paintModifier(KeyEvent.KEYCODE_META_LEFT, win); + for (var entry : shiftableKeys.entrySet()) + entry.getKey().setText(shift ? entry.getValue().shiftLabel : entry.getValue().label); + } + + /** Repaint the Extra/FNx toggles to match the zones actually on screen. */ + public void setZoneToggleState(boolean extraOn, boolean fnxOn) { + for (var btn : extraZoneButtons) paintToggle(btn, extraOn); + for (var btn : fnxZoneButtons) paintToggle(btn, fnxOn); + } + + private void paintModifier(int keyCode, boolean active) { + var buttons = modifierButtons.get(keyCode); + if (buttons == null) return; + for (var btn : buttons) paintToggle(btn, active); + } + + /** Works for both kinds of key: a text {@link Button} and an icon-only {@link ImageButton}. */ + private void paintToggle(@NonNull View key, boolean active) { + int color = getContext().getColor( + active ? R.color.extra_key_text_active : R.color.extra_key_text); + if (active) key.setBackgroundColor(getContext().getColor(R.color.extra_key_bg_active)); + else key.setBackgroundResource(R.drawable.extra_key_bg); + if (key instanceof Button) ((Button) key).setTextColor(color); + else if (key instanceof ImageButton) + ((ImageButton) key).setImageTintList(ColorStateList.valueOf(color)); + } + + public void setVisibleAnimated(boolean visible) { + ViewHeightAnimator.setVisible(this, visible); + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/DisplaySource.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/DisplaySource.java new file mode 100644 index 000000000..ef4adfe2a --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/DisplaySource.java @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. +package cn.classfun.droidvm.ui.vm.display.base; + +import androidx.annotation.NonNull; + +/** + * Pluggable VM display source: the thing that puts guest pixels on screen. The display activities + * only talk to this interface plus {@link DisplayViewportController}/{@link + * DisplayChromeController}, so a new source (e.g. a zero-copy AHardwareBuffer shared straight + * into the guest so it draws without a CPU copy) slots in without touching the console logic. + * + * Implementations today: {@code NativeSurfaceSource} (crosvm renders into an Android Surface via + * the per-VM ICrosvmAndroidDisplayService) and {@code VncBitmapSource} (RFB framebuffer copies + * into a bitmap-backed view). + */ +public interface DisplaySource { + enum State { + CONNECTING, + CONNECTED, + ERROR + } + + interface Callbacks { + /** + * Guest/framebuffer resolution known or changed - feed + * {@link DisplayViewportController#setContentSize}. In auto-resize mode this is also the + * ack of a {@link #requestGuestResize} request. + */ + void onContentSize(int width, int height); + + /** Connection-level state for the status bar / connecting overlay. */ + void onStateChanged(@NonNull State state); + } + + /** Start producing frames. Idempotent; sources that connect on their own may no-op. */ + void start(); + + /** Stop producing frames and release resources; the source is not reusable afterwards. */ + void shutdown(); + + /** + * Whether the guest display resolution can be changed at runtime (Auto-resize Guest + * Display). Sources without a resize channel return false and {@link #requestGuestResize} + * is a no-op; {@link DisplayViewportController#setAutoResize} must not be enabled for them. + */ + boolean supportsGuestResize(); + + /** Ask the guest display to switch to width x height; the ack arrives via onContentSize. */ + void requestGuestResize(int width, int height); + + /** + * Give this source a view to host the guest's HARDWARE cursor, or null for none. + * + * Only meaningful where the transport carries the cursor plane separately from the scanout + * (the native crosvm path does; VNC composites it server-side), so the default ignores it. + */ + default void setCursorView(android.view.SurfaceView cursorView) { + } + + /** + * Observe the guest's cursor position, in GUEST framebuffer coordinates, on the main thread. + * Default: no such channel, never called. + */ + default void setCursorListener(java.util.function.BiConsumer listener) { + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/DisplayViewportController.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/DisplayViewportController.java new file mode 100644 index 000000000..9f0a04313 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/DisplayViewportController.java @@ -0,0 +1,262 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. +package cn.classfun.droidvm.ui.vm.display.base; + +import androidx.annotation.NonNull; + +/** + * Single source of truth for the VM display viewport: where the guest image sits inside the + * display area, at what scale, under every combination of chrome (toolbar/status bar/extra + * keys/IME/system bars) visibility. Pure math, no Android view types, so the rules are unit + * testable; the owning activity feeds it sizes and gestures and applies the emitted geometry. + * + * State model: {@code scale} is absolute (screen px per content px) and {@code offset} is the + * content center relative to the display-area center. The invariant rules: + * + *
      + *
    • Fitted (scale at the letterbox fit): any display-area change re-fits automatically, so + * a "fit to window" view always stays fit-to-window.
    • + *
    • Zoomed: a display-area change keeps the absolute scale and the center offset - the image + * does not change size on screen, its center just follows the area center - then the offset + * is clamped back into the pan bounds. If the area grew enough that the fit scale catches + * up with the current scale, it snaps back to fitted.
    • + *
    • Degenerate area (e.g. landscape with a tall IME squeezing the container below + * {@code minAreaPx}, possibly to zero or negative): freeze. No geometry is recomputed or + * emitted, everything holds its last good position until the area recovers.
    • + *
    • Auto-resize mode (guest display follows the area): zoom gestures are disabled, every + * accepted area change re-fits and asks the listener to resize the guest. Debouncing the + * guest resize request is the caller's job.
    • + *
    + * + * The emitted geometry maps onto the existing view mechanics: lay the content view out at + * {@code baseW x baseH} centered in the container (the letterbox fit), then apply + * {@code viewScale} and translate by {@code offset}. + */ +public final class DisplayViewportController { + /** Maximum zoom, relative to the letterbox fit scale. */ + public static final float MAX_ZOOM = 5f; + // A scale within 0.1% of the fit scale counts as fitted (float noise + snap-to-fit). + private static final float FIT_SNAP = 1.001f; + + public interface Listener { + /** + * Apply the viewport: content view laid out at baseW x baseH centered in the area, then + * scaled by viewScale (1 = fitted) and translated by offset px. + */ + void onViewportChanged(int baseW, int baseH, float viewScale, float offsetX, float offsetY); + + /** Auto-resize mode only: the area changed; ask the guest display to match it. */ + void onGuestResizeWanted(int areaW, int areaH); + } + + private final int minAreaPx; + private final Listener listener; + + private int contentW, contentH; // guest/framebuffer resolution + private int areaW, areaH; // last accepted (non-degenerate) display area + private float scale; // absolute: screen px per content px + private float offsetX, offsetY; // content center relative to area center, screen px + private boolean autoResize; + private boolean frozen; + + /** + * @param minAreaPx area dimensions below this freeze the viewport instead of re-laying out. + */ + public DisplayViewportController(int minAreaPx, @NonNull Listener listener) { + this.minAreaPx = Math.max(1, minAreaPx); + this.listener = listener; + } + + /** Guest/framebuffer resolution known or changed. Always re-fits (a resolution change is a + * content-level event; in auto-resize mode it is the ack of a resize request). */ + public void setContentSize(int w, int h) { + if (w <= 0 || h <= 0 || (w == contentW && h == contentH)) { + return; + } + contentW = w; + contentH = h; + if (areaW <= 0 || areaH <= 0) { + // No display area yet (content size can arrive before the first layout pass, e.g. + // from the launch intent in onCreate): fitScale() would be 0 and the emitted + // viewScale NaN. Hold; the first setArea() re-fits and emits. + return; + } + fit(); + emit(); + } + + /** Display-area (container) size changed. This is the single entry point for every chrome/IME/ + * rotation-driven layout change; the caller does not need to know what caused it. */ + public void setArea(int w, int h) { + if (w < minAreaPx || h < minAreaPx) { + // Degenerate (possibly zero/negative under an oversized IME): hold everything. + frozen = true; + return; + } + frozen = false; + if (w == areaW && h == areaH) { + return; // no-op; layout listeners re-fire liberally + } + boolean hadArea = areaW > 0 && areaH > 0; + boolean wasFitted = hadArea && ready() && scale <= fitScale() * FIT_SNAP; + areaW = w; + areaH = h; + if (!ready()) { + return; + } + if (autoResize) { + fit(); + emit(); + listener.onGuestResizeWanted(w, h); + return; + } + if (!hadArea || wasFitted || scale <= fitScale() * FIT_SNAP) { + // Was fit-to-window, or the area grew past the current zoom: back to fit. + fit(); + } else { + // Zoomed: same on-screen size, same center offset, clamped into the new bounds. + clampOffset(); + } + emit(); + } + + /** + * Three-finger zoom/pan gesture step. {@code scaleFactor} is relative to the current scale; + * dx/dy are pan deltas in screen px. Ignored while frozen or in auto-resize mode. + */ + public void onZoomPan(float scaleFactor, float dxPx, float dyPx) { + if (!ready() || areaW == 0 || frozen || autoResize) { + return; + } + float fit = fitScale(); + float next = clamp(scale * scaleFactor, fit, fit * MAX_ZOOM); + if (next <= fit * FIT_SNAP) { + fit(); // snap back to fit; a fitted view has nothing to pan + } else { + scale = next; + offsetX += dxPx; + offsetY += dyPx; + clampOffset(); + } + emit(); + } + + /** + * Pan the smallest amount that brings a GUEST-space point back inside the visible area, with + * {@code marginPx} of screen-space breathing room around it. No-op unless zoomed in. + * + * This is what makes the view follow the guest's pointer while zoomed: the cursor can leave + * the visible rectangle without the user ever touching the edge of the screen, because in + * relative-pointer mode a small finger drag can move the guest cursor a long way. + * + * Deliberately the SMALLEST correction rather than re-centring on the point: re-centring turns + * every cursor movement near an edge into a large view jump, which is far more disorienting + * than the pointer briefly reaching the edge. When the view is fitted there is nothing + * off-screen to reveal, and clampOffset would undo any pan anyway, so it returns early. + * + * @return true if the viewport actually moved. + */ + public boolean panToShowContentPoint(float gx, float gy, float marginPx) { + if (!ready() || areaW == 0 || frozen || autoResize || isFitted()) { + return false; + } + // Content is drawn centred on the area, then displaced by offset. + float sx = areaW / 2f + offsetX + (gx - contentW / 2f) * scale; + float sy = areaH / 2f + offsetY + (gy - contentH / 2f) * scale; + + // A margin wider than half the area would fight itself on both edges at once. + float mx = Math.min(marginPx, areaW / 2f); + float my = Math.min(marginPx, areaH / 2f); + + float beforeX = offsetX, beforeY = offsetY; + if (sx < mx) { + offsetX += mx - sx; + } else if (sx > areaW - mx) { + offsetX -= sx - (areaW - mx); + } + if (sy < my) { + offsetY += my - sy; + } else if (sy > areaH - my) { + offsetY -= sy - (areaH - my); + } + if (offsetX == beforeX && offsetY == beforeY) { + return false; + } + clampOffset(); + if (offsetX == beforeX && offsetY == beforeY) { + return false; // the pan was entirely clamped away; do not emit a no-change frame + } + emit(); + return true; + } + + /** Back to fit-to-window (e.g. on input-mode switch). */ + public void resetToFit() { + if (!ready() || areaW == 0) { + return; + } + fit(); + emit(); + } + + /** Guest display follows the area: zoom disabled, area changes request a guest resize. */ + public void setAutoResize(boolean enabled) { + if (autoResize == enabled) { + return; + } + autoResize = enabled; + if (enabled && ready() && areaW > 0) { + fit(); + emit(); + listener.onGuestResizeWanted(areaW, areaH); + } + } + + public boolean isFitted() { + return ready() && areaW > 0 && scale <= fitScale() * FIT_SNAP; + } + + public boolean isFrozen() { + return frozen; + } + + /** Absolute scale, screen px per content px. */ + public float getScale() { + return scale; + } + + private boolean ready() { + return contentW > 0 && contentH > 0; + } + + private float fitScale() { + return Math.min(areaW / (float) contentW, areaH / (float) contentH); + } + + private void fit() { + scale = fitScale(); + offsetX = 0; + offsetY = 0; + } + + // Pan bounds: the image may only pan along an axis where it overflows the area, and never far + // enough to pull its edge inside the area (no gaps while zoomed). Letterboxed axes lock to 0. + private void clampOffset() { + float halfGapX = Math.max(0, (contentW * scale - areaW) / 2f); + float halfGapY = Math.max(0, (contentH * scale - areaH) / 2f); + offsetX = clamp(offsetX, -halfGapX, halfGapX); + offsetY = clamp(offsetY, -halfGapY, halfGapY); + } + + private void emit() { + float fit = fitScale(); + int baseW = Math.round(contentW * fit); + int baseH = Math.round(contentH * fit); + listener.onViewportChanged(baseW, baseH, scale / fit, offsetX, offsetY); + } + + private static float clamp(float v, float lo, float hi) { + return Math.max(lo, Math.min(v, hi)); + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/HoldKeyGroup.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/HoldKeyGroup.java new file mode 100644 index 000000000..e3ae38e08 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/HoldKeyGroup.java @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. +package cn.classfun.droidvm.ui.vm.display.base; + +import static android.view.HapticFeedbackConstants.KEYBOARD_TAP; + +import android.annotation.SuppressLint; +import android.graphics.Rect; +import android.view.MotionEvent; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Hold-to-press keys with glide, for one keyboard widget: finger down on a key sends its key-down; + * sliding onto another registered key while held sends that key's down and the previous key's up + * (down-before-up, so a W-to-A roll never has a gap); release - or sliding off every key - sends + * key-up. The guest sees real holds, so auto-repeat and hold semantics are its own, and several + * pointers can hold several keys at once (each pointer's stream stays with its origin view; this + * group just retargets which key that pointer currently presses). + * + * Sticky modifiers are deliberately NOT registered here: gliding across one must not toggle it. + */ +public final class HoldKeyGroup { + public interface Sink { + void onKey(int androidKeyCode, boolean down); + } + + private static final class Entry { + final View view; + final int keyCode; + + Entry(View view, int keyCode) { + this.view = view; + this.keyCode = keyCode; + } + } + + private final List entries = new ArrayList<>(); + @NonNull + private final Sink sink; + + public HoldKeyGroup(@NonNull Sink sink) { + this.sink = sink; + } + + @SuppressLint("ClickableViewAccessibility") + public void register(@NonNull View view, int keyCode) { + var entry = new Entry(view, keyCode); + entries.add(entry); + view.setOnTouchListener(new PointerTracker(entry)); + } + + private void press(@NonNull Entry e) { + e.view.setPressed(true); + e.view.performHapticFeedback(KEYBOARD_TAP); + sink.onKey(e.keyCode, true); + } + + private void release(@NonNull Entry e) { + e.view.setPressed(false); + sink.onKey(e.keyCode, false); + } + + /** Tracks the single pointer whose ACTION_DOWN landed on one origin key. */ + private final class PointerTracker implements View.OnTouchListener { + private final Entry origin; + // Screen-space bounds of every visible registered key, cached per gesture (layout does + // not change mid-touch). + private final Map bounds = new HashMap<>(); + @Nullable + private Entry current; + + PointerTracker(@NonNull Entry origin) { + this.origin = origin; + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + cacheBounds(); + current = origin; + press(origin); + return true; + case MotionEvent.ACTION_MOVE: + onMove(event.getRawX(), event.getRawY()); + return true; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + if (current != null) { + release(current); + current = null; + } + return true; + } + return false; + } + + private void onMove(float rawX, float rawY) { + int x = Math.round(rawX), y = Math.round(rawY); + if (current != null) { + var rect = bounds.get(current); + if (rect != null && rect.contains(x, y)) return; + } + var target = hitTest(x, y); + if (target == current) return; + // Down-before-up on a roll so the guest never sees a hold gap. + if (target != null) press(target); + if (current != null) release(current); + current = target; + } + + @Nullable + private Entry hitTest(int x, int y) { + for (var e : entries) { + var rect = bounds.get(e); + // A key another pointer is holding can't be glided onto. + if (rect != null && rect.contains(x, y) && !e.view.isPressed()) return e; + } + return null; + } + + private void cacheBounds() { + bounds.clear(); + int[] loc = new int[2]; + for (var e : entries) { + if (!e.view.isShown()) continue; + e.view.getLocationOnScreen(loc); + bounds.put(e, new Rect( + loc[0], loc[1], loc[0] + e.view.getWidth(), loc[1] + e.view.getHeight())); + } + } + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/InputMode.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/InputMode.java new file mode 100644 index 000000000..dd031b6eb --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/InputMode.java @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. +package cn.classfun.droidvm.ui.vm.display.base; + +import androidx.annotation.NonNull; +import androidx.annotation.StringRes; + +import cn.classfun.droidvm.R; + +/** + * Pointer input mode shared by both display backends (VNC and native). The control bar exposes a + * selector; each {@link DisplayInputBackend} interprets the current mode when it turns on-screen + * touches into guest input: + * + *
      + * contacts preserved (the default; what a touchscreen guest expects). + * emulation. Android has no absolute-mouse concept, so this is the only faithful mouse path + * for guests that want relative motion (FPS games, desktops with pointer acceleration). + * absolute guest coordinates, no multi-finger gestures. + *
    + * + * The ordinal is persisted (SharedPreferences {@code display_input_mode}); TOUCH/MOUSE keep their + * historical 0/1 values so existing settings stay valid, TABLET is appended as 2. + */ +public enum InputMode { + TOUCH(R.string.vnc_menu_input_mode_touch), + MOUSE(R.string.vnc_menu_input_mode_mouse), + TABLET(R.string.vnc_menu_input_mode_tablet); + + @StringRes + public final int labelResId; + + InputMode(@StringRes int labelResId) { + this.labelResId = labelResId; + } + + /** Persisted-ordinal lookup that never throws; unknown/out-of-range values fall back to TOUCH. */ + @NonNull + public static InputMode fromOrdinal(int ordinal) { + InputMode[] values = values(); + return (ordinal >= 0 && ordinal < values.length) ? values[ordinal] : TOUCH; + } + + /** The next mode in the cycle, for a single toggle button that rotates TOUCH -> MOUSE -> TABLET. */ + @NonNull + public InputMode next() { + InputMode[] values = values(); + return values[(ordinal() + 1) % values.length]; + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/KeyListener.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/KeyListener.java index 48dc5a234..462883e45 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/KeyListener.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/KeyListener.java @@ -4,18 +4,14 @@ package cn.classfun.droidvm.ui.vm.display.base; public interface KeyListener { - @SuppressWarnings("unused") - void onKeyRepeat(int androidKeyCode); + /** + * A non-modifier key went down ({@code down=true}) or up. The guest sees the real hold, so + * auto-repeat and hold semantics (e.g. WASD movement) are the guest's own; several keys may + * be held at once. + */ + void onKey(int androidKeyCode, boolean down); - @SuppressWarnings("unused") - void onCharRepeat(char ch); - - @SuppressWarnings("unused") - void onCapsToggle(boolean active); - - @SuppressWarnings("unused") void onModifierClick(int androidKeyCode); - @SuppressWarnings("unused") void onModifierLongClick(int androidKeyCode); } diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/KeyboardMode.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/KeyboardMode.java new file mode 100644 index 000000000..fd34c92c1 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/KeyboardMode.java @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. +package cn.classfun.droidvm.ui.vm.display.base; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; + +import cn.classfun.droidvm.R; + +/** + * Which typing surface the display shows. Exactly one is active, and it decides what the Main + * zone of the on-screen keyboard is; the Extra and FNx zones are independent toggles that only + * apply while a keyboard is up ({@link #NONE} hides everything without forgetting them). + */ +public enum KeyboardMode { + /** Nothing shown - the display gets the whole screen. */ + NONE(R.string.keyboard_mode_none), + /** Main is one row of the keys a soft keyboard lacks; text comes from the system IME. */ + SYSTEM(R.string.keyboard_mode_system), + /** Main is the on-screen laptop keyboard; no IME involved. */ + LAPTOP(R.string.keyboard_mode_laptop); + + private final @StringRes int stringId; + + KeyboardMode(@StringRes int stringId) { + this.stringId = stringId; + } + + @StringRes + public int getStringId() { + return stringId; + } + + public boolean showsKeyboard() { + return this != NONE; + } + + @NonNull + public static KeyboardMode fromName(@Nullable String name) { + if (name != null) { + for (var m : values()) + if (m.name().equals(name)) return m; + } + return SYSTEM; + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/PointerGestureTranslator.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/PointerGestureTranslator.java new file mode 100644 index 000000000..8387d7fc9 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/PointerGestureTranslator.java @@ -0,0 +1,467 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. +package cn.classfun.droidvm.ui.vm.display.base; + +import android.graphics.RectF; +import android.os.Handler; +import android.view.MotionEvent; + +import androidx.annotation.NonNull; + +/** + * Unified pointer-gesture state machine for the MOUSE and TABLET input modes, shared by the VNC and + * native display paths. The activity feeds raw touch {@link MotionEvent}s in the coordinates of the + * gesture surface (the whole display container, letterbox included) together with the rectangle the + * guest frame is actually rendered into; the translator classifies them and emits semantic + * callbacks; each display backend maps those onto its wire format (evdev via crosvm --input, or RFB + * pointer events). + * + * Gestures: + *
      + * = left click, tap-and-a-half (tap, then touch-and-move within the tap-drag window) = left + * drag with no leading click, libinput-style. TABLET: press is committed as a left-button-down + * at the absolute position after a short defer window (so a second finger can still turn it + * into a right-click), then drag draws; quick tap = left click. + * time < threshold and it barely moved) = right click, positioned at finger 1 (tablet). + * Two fingers panning together = scroll-wheel notches. + * the display view only, never sent to the guest. + *
    + * + * Active area: the whole gesture surface is usable; only gestures that carry a coordinate to the + * guest are pinned to the display rect. MOUSE (relative) is never pinned. TABLET pins on where + * finger 1 went down: press/drag/tap, the right-click anchor and the scroll anchor (the scroll's + * coordinate is sampled at press time; the panning after it may leave the rect freely). The + * second finger of a right-click and the three-finger zoom/pan are position-free and work + * anywhere, letterbox included. + * + * TOUCH (multi-touch) mode bypasses this class entirely. + * + * All methods run on the UI thread (touch dispatch). The tablet press-defer uses the supplied + * {@link Handler} (main looper). + */ +public final class PointerGestureTranslator { + public interface Listener { + /** MOUSE mode: relative cursor motion, already scaled to guest pixels. */ + void onRelativeMove(float dxGuest, float dyGuest); + + /** TABLET mode: absolute pointer position (no button change), guest pixels. */ + void onAbsoluteMove(float xGuest, float yGuest); + + /** Left button transition. TABLET uses the coordinates; MOUSE may ignore them. */ + void onLeftButton(boolean down, float xGuest, float yGuest); + + /** Quick single-finger tap = left click (down+up). */ + void onLeftTap(float xGuest, float yGuest); + + /** Two-finger quick tap = right click; coordinates are finger 1's position (tablet). */ + void onRightClick(float xGuest, float yGuest); + + /** Two-finger pan, quantized to wheel notches (+v = scroll up, +h = scroll right). */ + void onScroll(int vNotches, int hNotches); + + /** + * Three-finger pinch/pan: local display transform. scaleFactor is relative to the previous + * event; pan/focus are view pixels. Never forwarded to the guest. + */ + void onZoomPan(float scaleFactor, float dxView, float dyView, float focusX, float focusY); + } + + private static final long TAP_MS = 250; // max press duration for a tap + private static final long RIGHT_TAP_MS = 300; // max finger-2 press duration for right click + // Mouse tap-and-drag window (libinput-style): after a tap the left button is held DOWN, and its + // release is deferred this long. A finger returning within the window continues the SAME press + // -- moving it drags (one clean press, no leading click); a second quick tap becomes a + // double-click; nothing means a plain click completes. Kept short so single clicks feel + // immediate; the tradeoff is the tap-drag re-touch must be quick. + private static final long TAP_DRAG_MS = 100; + private static final long TABLET_DEFER_MS = 60; // tablet: defer left-down so 2nd finger can veto + private static final float TAP_SLOP = 18f; // view px of travel before a press is a drag + private static final float SCROLL_NOTCH_PX = 64f; // view px of two-finger pan per wheel notch + + // TAP_WAIT: mouse tap done, left button held, waiting out the tap-drag window for a return + // touch. DRAG_HELD: a finger returned within the window, button still held, not yet moved far + // enough to be a drag (vs a double-tap). + private enum State {IDLE, PENDING1, DRAG_MOVE, DRAG_LEFT, TAP_WAIT, DRAG_HELD, TWO, SCROLL2, + THREE, DEAD} + + private final Handler handler; + private final Listener listener; + private boolean absolute; // tablet=true, mouse=false + + private State state = State.IDLE; + // display rect + surface->guest scale, refreshed on every event so layout changes are picked up + private final RectF dispRect = new RectF(0, 0, 1, 1); + private float guestW = 1f, guestH = 1f; + private float scaleX = 1f, scaleY = 1f; + + private int finger1Id = -1; + private float f1DownX, f1DownY, f1LastX, f1LastY; + private long f1DownTime; + // Finger 1 went down inside the display rect: TABLET coordinate ops (press/tap/drag, + // right-click anchor, scroll anchor) are only valid then. Irrelevant in MOUSE mode. + private boolean f1InDisplay; + + private int finger2Id = -1; + private float f2DownX, f2DownY; + private long f2DownTime; + private boolean twoMoved; + private float scrollAccumV, scrollAccumH; + private float twoLastMidX, twoLastMidY; + + private float threeLastCx, threeLastCy, threeLastSpread; + + private final Runnable tabletCommit = this::commitTabletPress; + private final Runnable tapRelease = this::commitTapRelease; + + public PointerGestureTranslator(@NonNull Handler handler, @NonNull Listener listener) { + this.handler = handler; + this.listener = listener; + } + + /** Switch between TABLET (absolute) and MOUSE (relative) semantics. Resets in-flight state. */ + public void setAbsolute(boolean absolute) { + if (this.absolute != absolute) reset(); + this.absolute = absolute; + } + + public void reset() { + handler.removeCallbacks(tabletCommit); + handler.removeCallbacks(tapRelease); + // Never leave the guest's left button stuck down if we tear down mid-press. + if (state == State.DRAG_LEFT || state == State.DRAG_HELD || state == State.TAP_WAIT) + listener.onLeftButton(false, guestX(f1LastX), guestY(f1LastY)); + state = State.IDLE; + finger1Id = -1; + finger2Id = -1; + } + + /** + * Feeds one touch event. Returns true if consumed. + * + * @param displayRect where the guest frame is rendered, in the event's coordinate space (the + * letterbox-fitted display view under any local zoom/pan transform) + * @param guestWidth guest coordinate range the display rect maps onto (fb px or the + * normalized evdev ABS range, whatever the backend's wire format wants) + * @param guestHeight guest coordinate range the display rect maps onto + */ + public boolean onTouchEvent(@NonNull MotionEvent ev, @NonNull RectF displayRect, + float guestWidth, float guestHeight) { + if (displayRect.width() <= 0f || displayRect.height() <= 0f) return false; + dispRect.set(displayRect); + guestW = guestWidth; + guestH = guestHeight; + scaleX = guestWidth / displayRect.width(); + scaleY = guestHeight / displayRect.height(); + switch (ev.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + onFirstDown(ev); + return true; + case MotionEvent.ACTION_POINTER_DOWN: + onExtraDown(ev); + return true; + case MotionEvent.ACTION_MOVE: + onMove(ev); + return true; + case MotionEvent.ACTION_POINTER_UP: + onPointerUp(ev); + return true; + case MotionEvent.ACTION_UP: + onLastUp(ev); + return true; + case MotionEvent.ACTION_CANCEL: + cancel(); + return true; + default: + return false; + } + } + + /** Maps a gesture-surface coordinate to guest px, clamped to the guest bounds. */ + private float guestX(float x) { + return Math.max(0f, Math.min((x - dispRect.left) * scaleX, guestW - 1f)); + } + + private float guestY(float y) { + return Math.max(0f, Math.min((y - dispRect.top) * scaleY, guestH - 1f)); + } + + /** Whether TABLET coordinate ops are valid for this gesture (always true for MOUSE). */ + private boolean coordOps() { + return !absolute || f1InDisplay; + } + + private void onFirstDown(MotionEvent ev) { + finger1Id = ev.getPointerId(0); + f1DownX = f1LastX = ev.getX(0); + f1DownY = f1LastY = ev.getY(0); + f1DownTime = ev.getEventTime(); + // Mouse: a finger returning while the previous tap's release is still deferred continues + // the SAME held press. Moving it becomes a drag (no leading click); a quick lift is the + // second half of a double-click. See onLastUp/onMove for the split. + if (!absolute && state == State.TAP_WAIT) { + handler.removeCallbacks(tapRelease); + state = State.DRAG_HELD; + return; + } + f1InDisplay = dispRect.contains(f1DownX, f1DownY); + state = State.PENDING1; + if (absolute && f1InDisplay) { + // Tablet: commit the press after the defer window unless a second finger vetoes it. + // A press outside the display rect has no guest coordinate and never commits; the + // fingers are still tracked so multi-finger gestures can form from the letterbox. + handler.postDelayed(tabletCommit, TABLET_DEFER_MS); + } + } + + private void commitTabletPress() { + if (state != State.PENDING1 || !absolute) return; + state = State.DRAG_LEFT; + listener.onLeftButton(true, guestX(f1LastX), guestY(f1LastY)); + } + + // Mouse tap-drag window expired with no return touch: the deferred tap is just a click. + private void commitTapRelease() { + if (state != State.TAP_WAIT) return; + listener.onLeftButton(false, guestX(f1LastX), guestY(f1LastY)); + state = State.IDLE; + finger1Id = -1; + } + + private void onExtraDown(MotionEvent ev) { + int count = ev.getPointerCount(); + if (count == 2 && (state == State.PENDING1 || state == State.DRAG_MOVE + || state == State.DRAG_LEFT || state == State.DRAG_HELD)) { + handler.removeCallbacks(tabletCommit); + if (state == State.DRAG_LEFT || state == State.DRAG_HELD) { + // The press was already committed (or held from a tap); release it before + // switching to the two-finger gesture. + listener.onLeftButton(false, guestX(f1LastX), guestY(f1LastY)); + } + int idx = ev.getActionIndex(); + finger2Id = ev.getPointerId(idx); + f2DownX = ev.getX(idx); + f2DownY = ev.getY(idx); + f2DownTime = ev.getEventTime(); + twoMoved = false; + scrollAccumV = 0; + scrollAccumH = 0; + twoLastMidX = (f1LastX + f2DownX) / 2f; + twoLastMidY = (f1LastY + f2DownY) / 2f; + state = State.TWO; + } else if (count == 3 && (state == State.TWO || state == State.SCROLL2)) { + state = State.THREE; + threeLastCx = centroidX(ev); + threeLastCy = centroidY(ev); + threeLastSpread = spread(ev); + } else { + // 4+ fingers or unexpected ordering: bail until all fingers lift. + handler.removeCallbacks(tabletCommit); + state = State.DEAD; + } + } + + private void onMove(MotionEvent ev) { + switch (state) { + case PENDING1: { + int idx = ev.findPointerIndex(finger1Id); + if (idx < 0) return; + float x = ev.getX(idx), y = ev.getY(idx); + if (Math.abs(x - f1DownX) > TAP_SLOP || Math.abs(y - f1DownY) > TAP_SLOP) { + handler.removeCallbacks(tabletCommit); + if (absolute) { + if (f1InDisplay) { + // Commit press at the original touch point, then drag from there. + listener.onLeftButton(true, guestX(f1DownX), guestY(f1DownY)); + state = State.DRAG_LEFT; + } + // Outside the display rect a tablet drag has no coordinate; stay + // PENDING1 so extra fingers can still form the multi-finger gestures. + } else { + state = State.DRAG_MOVE; + } + } + if (coordOps()) emitPointerMove(x, y); + f1LastX = x; + f1LastY = y; + break; + } + case DRAG_MOVE: + case DRAG_LEFT: { + int idx = ev.findPointerIndex(finger1Id); + if (idx < 0) return; + float x = ev.getX(idx), y = ev.getY(idx); + emitPointerMove(x, y); + f1LastX = x; + f1LastY = y; + break; + } + case DRAG_HELD: { + // Return touch of a tap-drag: the left button is already held. Once it travels + // past the slop it's a drag; a lift before that (in onLastUp) is a double-click. + int idx = ev.findPointerIndex(finger1Id); + if (idx < 0) return; + float x = ev.getX(idx), y = ev.getY(idx); + if (Math.abs(x - f1DownX) > TAP_SLOP || Math.abs(y - f1DownY) > TAP_SLOP) + state = State.DRAG_LEFT; + if (state == State.DRAG_LEFT) emitPointerMove(x, y); + f1LastX = x; + f1LastY = y; + break; + } + case TWO: + case SCROLL2: { + int i1 = ev.findPointerIndex(finger1Id); + int i2 = ev.findPointerIndex(finger2Id); + if (i1 < 0 || i2 < 0) return; + float midX = (ev.getX(i1) + ev.getX(i2)) / 2f; + float midY = (ev.getY(i1) + ev.getY(i2)) / 2f; + float dx = midX - twoLastMidX; + float dy = midY - twoLastMidY; + if (state == State.TWO + && (Math.abs(midX - (f1DownX + f2DownX) / 2f) > TAP_SLOP + || Math.abs(midY - (f1DownY + f2DownY) / 2f) > TAP_SLOP)) { + twoMoved = true; + if (coordOps()) { + state = State.SCROLL2; + // The scroll's coordinate is taken at press time: pin the guest pointer + // to finger 1's down position so the wheel lands under the fingers; the + // panning after this is position-free and may leave the display rect. + if (absolute) + listener.onAbsoluteMove(guestX(f1DownX), guestY(f1DownY)); + } + } + if (state == State.SCROLL2) { + // Natural scrolling: content follows the fingers (pan down = wheel up). + scrollAccumV += dy; + scrollAccumH += dx; + int v = (int) (scrollAccumV / SCROLL_NOTCH_PX); + int h = (int) (scrollAccumH / SCROLL_NOTCH_PX); + if (v != 0 || h != 0) { + scrollAccumV -= v * SCROLL_NOTCH_PX; + scrollAccumH -= h * SCROLL_NOTCH_PX; + listener.onScroll(v, -h); + } + } + twoLastMidX = midX; + twoLastMidY = midY; + f1LastX = ev.getX(i1); + f1LastY = ev.getY(i1); + break; + } + case THREE: { + if (ev.getPointerCount() < 3) return; + float cx = centroidX(ev), cy = centroidY(ev); + float sp = spread(ev); + float factor = threeLastSpread > 1f ? sp / threeLastSpread : 1f; + listener.onZoomPan(factor, cx - threeLastCx, cy - threeLastCy, cx, cy); + threeLastCx = cx; + threeLastCy = cy; + threeLastSpread = sp; + break; + } + default: + break; + } + } + + private void emitPointerMove(float x, float y) { + if (absolute) { + listener.onAbsoluteMove(guestX(x), guestY(y)); + } else { + listener.onRelativeMove((x - f1LastX) * scaleX, (y - f1LastY) * scaleY); + } + } + + private void onPointerUp(MotionEvent ev) { + int id = ev.getPointerId(ev.getActionIndex()); + if ((state == State.TWO || state == State.SCROLL2) && id == finger2Id) { + long held = ev.getEventTime() - f2DownTime; + if (state == State.TWO && !twoMoved && held <= RIGHT_TAP_MS && coordOps()) { + // 1 -> 2 -> 1 with a quick second finger: right click at finger 1's position + // (finger 2 is position-free and may sit outside the display rect). + listener.onRightClick(guestX(f1LastX), guestY(f1LastY)); + } + // Whatever remains of the gesture is spent; ignore finger 1 until it lifts. + state = State.DEAD; + finger2Id = -1; + } else if ((state == State.TWO || state == State.SCROLL2) && id == finger1Id) { + // Finger 1 left first; treat the same as gesture end. + state = State.DEAD; + } else if (state == State.THREE && ev.getPointerCount() - 1 < 3) { + state = State.DEAD; + } + } + + private void onLastUp(MotionEvent ev) { + handler.removeCallbacks(tabletCommit); + switch (state) { + case PENDING1: { + long held = ev.getEventTime() - f1DownTime; + if (held <= TAP_MS) { + if (absolute) { + if (f1InDisplay) listener.onLeftTap(guestX(f1DownX), guestY(f1DownY)); + } else { + // Mouse tap: press now but defer the release, so a finger returning within + // the window continues it as a drag (no leading click) or a double-click. + listener.onLeftButton(true, guestX(f1DownX), guestY(f1DownY)); + state = State.TAP_WAIT; + handler.postDelayed(tapRelease, TAP_DRAG_MS); + return; // keep TAP_WAIT; don't fall through to the IDLE reset + } + } else if (absolute && f1InDisplay) { + // Long still hold that never got committed: emit press+release at the position. + listener.onLeftButton(true, guestX(f1DownX), guestY(f1DownY)); + listener.onLeftButton(false, guestX(f1DownX), guestY(f1DownY)); + } + break; + } + case DRAG_HELD: + // Return touch lifted without dragging: it was a double-tap. Release the held + // press (completes click 1), then emit a second click. + listener.onLeftButton(false, guestX(f1LastX), guestY(f1LastY)); + listener.onLeftButton(true, guestX(f1LastX), guestY(f1LastY)); + listener.onLeftButton(false, guestX(f1LastX), guestY(f1LastY)); + break; + case DRAG_LEFT: + listener.onLeftButton(false, guestX(f1LastX), guestY(f1LastY)); + break; + default: + break; + } + state = State.IDLE; + finger1Id = -1; + finger2Id = -1; + } + + private void cancel() { + handler.removeCallbacks(tabletCommit); + handler.removeCallbacks(tapRelease); + if (state == State.DRAG_LEFT || state == State.DRAG_HELD || state == State.TAP_WAIT) + listener.onLeftButton(false, guestX(f1LastX), guestY(f1LastY)); + state = State.IDLE; + finger1Id = -1; + finger2Id = -1; + } + + private static float centroidX(MotionEvent ev) { + float s = 0; + for (int i = 0; i < ev.getPointerCount(); i++) s += ev.getX(i); + return s / ev.getPointerCount(); + } + + private static float centroidY(MotionEvent ev) { + float s = 0; + for (int i = 0; i < ev.getPointerCount(); i++) s += ev.getY(i); + return s / ev.getPointerCount(); + } + + /** Mean distance of the pointers from their centroid; pinch ratio = spread/lastSpread. */ + private static float spread(MotionEvent ev) { + float cx = centroidX(ev), cy = centroidY(ev); + float s = 0; + for (int i = 0; i < ev.getPointerCount(); i++) + s += Math.hypot(ev.getX(i) - cx, ev.getY(i) - cy); + return s / ev.getPointerCount(); + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/ViewHeightAnimator.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/ViewHeightAnimator.java new file mode 100644 index 000000000..7174ec4a7 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/ViewHeightAnimator.java @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. +package cn.classfun.droidvm.ui.vm.display.base; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.view.View; + +import androidx.annotation.NonNull; + +import cn.classfun.droidvm.R; + +/** + * Slide a bottom-docked row/panel open or closed by animating its layout height. The view's + * pre-animation layout height (fixed dp or WRAP_CONTENT) is remembered on first use and restored + * when the animation finishes - blindly resetting to WRAP_CONTENT would permanently squash rows + * whose height comes from their own fixed layout_height (their match_parent children then wrap to + * text height). + */ +public final class ViewHeightAnimator { + private static final long DURATION = 200; + + private ViewHeightAnimator() { + } + + public static void setVisible(@NonNull View view, boolean visible) { + if (visible) show(view); + else hide(view); + } + + /** The view's own layout height before any animation touched it. */ + private static int originalHeight(@NonNull View view) { + Object tag = view.getTag(R.id.view_height_animator_original); + if (tag instanceof Integer) return (Integer) tag; + int height = view.getLayoutParams().height; + view.setTag(R.id.view_height_animator_original, height); + return height; + } + + public static void show(@NonNull View view) { + if (view.getVisibility() == View.VISIBLE) return; + int original = originalHeight(view); + view.setVisibility(View.VISIBLE); + int target; + if (original > 0) { + target = original; + } else { + view.measure( + View.MeasureSpec.makeMeasureSpec( + ((View) view.getParent()).getWidth(), View.MeasureSpec.EXACTLY), + View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) + ); + target = view.getMeasuredHeight(); + } + var lp = view.getLayoutParams(); + lp.height = 0; + view.requestLayout(); + var anim = ValueAnimator.ofInt(0, target); + anim.setDuration(DURATION); + anim.addUpdateListener(a -> { + lp.height = (int) a.getAnimatedValue(); + view.requestLayout(); + }); + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator a) { + lp.height = original; + view.requestLayout(); + } + }); + anim.start(); + } + + public static void hide(@NonNull View view) { + if (view.getVisibility() == View.GONE) return; + int original = originalHeight(view); + int start = view.getHeight(); + var lp = view.getLayoutParams(); + var anim = ValueAnimator.ofInt(start, 0); + anim.setDuration(DURATION); + anim.addUpdateListener(a -> { + lp.height = (int) a.getAnimatedValue(); + view.requestLayout(); + }); + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator a) { + view.setVisibility(View.GONE); + lp.height = original; + view.requestLayout(); + } + }); + anim.start(); + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/X11Keymap.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/X11Keymap.java index 37c5e8457..7f905be0f 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/X11Keymap.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/base/X11Keymap.java @@ -11,6 +11,7 @@ public final class X11Keymap { public static final int XK_Tab = 0xff09; /* U+0009 CHARACTER TABULATION */ public static final int XK_Clear = 0xff0b; /* U+000B LINE TABULATION */ public static final int XK_Return = 0xff0d; /* U+000D CARRIAGE RETURN */ + public static final int XK_Pause = 0xff13; public static final int XK_Scroll_Lock = 0xff14; public static final int XK_Sys_Req = 0xff15; public static final int XK_Escape = 0xff1b; /* U+001B ESCAPE */ @@ -235,10 +236,12 @@ public static int androidKeyToXKeysym(int keyCode) { return XK_Num_Lock; case KeyEvent.KEYCODE_FUNCTION: return XF86XK_Fn; + // The unmodified keys: X servers derive Sys_Req/Break themselves when Alt/Ctrl is + // held, and VNC-server keymaps often only know the unmodified symbols. case KeyEvent.KEYCODE_SYSRQ: - return XK_Sys_Req; + return XK_Print; case KeyEvent.KEYCODE_BREAK: - return XK_Break; + return XK_Pause; case KeyEvent.KEYCODE_SPACE: return XK_space; case KeyEvent.KEYCODE_GRAVE: diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/display/CursorPositionStream.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/display/CursorPositionStream.java new file mode 100644 index 000000000..4afb7718c --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/display/CursorPositionStream.java @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. +package cn.classfun.droidvm.ui.vm.display.nativedisplay.display; + +import android.crosvm.ICrosvmAndroidDisplayService; +import android.os.Handler; +import android.os.Looper; +import android.os.ParcelFileDescriptor; +import android.util.Log; + +import androidx.annotation.NonNull; + +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * The guest cursor's position, straight from virtio-gpu. + * + * crosvm's Android display backend already writes every cursor move to a pipe -- see + * set_android_surface_position() in crosvm_android_display_client.cpp, which does nothing but + * write(fd, {x, y}). Until now nothing ever called setCursorStream, so that fd stayed -1 and the + * backend logged "cursor position stream is not attached; dropping position updates" once and + * threw every update away. This class is the missing other end. + * + * WHY THIS AND NOT THE APP'S OWN TOUCH COORDINATES + * + * The app knows where the user touched, which is not where the guest's pointer is: + * - in relative-pointer mode the app sends DELTAS, so it has no absolute position at all; + * - the guest warps the pointer on its own (games grabbing it, dialogs centring it); + * - while zoomed there is a scale factor between the two, so any app-side dead reckoning drifts. + * These x/y come from the guest's own cursor plane, which is the only authoritative source. + * + * Values are little-endian u32 pairs in GUEST framebuffer coordinates. + */ +final class CursorPositionStream implements AutoCloseable { + + /** Delivered on the main thread. */ + interface Listener { + void onCursorMoved(int guestX, int guestY); + } + + private static final String TAG = "CursorPositionStream"; + + private final Handler mainHandler = new Handler(Looper.getMainLooper()); + private final Listener listener; + private ParcelFileDescriptor readSide; + private Thread reader; + private volatile boolean closed; + + private CursorPositionStream(@NonNull Listener listener) { + this.listener = listener; + } + + /** + * Creates the pipe, hands the WRITE end to crosvm and starts reading the other end. + * Returns null if the service refuses it, in which case the caller simply has no cursor + * position -- everything else keeps working. + */ + static CursorPositionStream attach(@NonNull ICrosvmAndroidDisplayService service, + @NonNull Listener listener) { + ParcelFileDescriptor[] pipe; + try { + pipe = ParcelFileDescriptor.createPipe(); + } catch (IOException e) { + Log.w(TAG, "could not create cursor pipe", e); + return null; + } + var self = new CursorPositionStream(listener); + self.readSide = pipe[0]; + try { + // The binder call dups the fd, so our copy must be closed either way -- crosvm keeps + // its own. Leaving it open here would mean the reader never sees EOF when crosvm exits. + service.setCursorStream(pipe[1]); + } catch (Exception e) { + Log.w(TAG, "setCursorStream failed; cursor position unavailable", e); + closeQuietly(pipe[0]); + closeQuietly(pipe[1]); + return null; + } finally { + closeQuietly(pipe[1]); + } + + self.reader = new Thread(self::readLoop, "CursorPositionStream"); + self.reader.setDaemon(true); + self.reader.start(); + Log.i(TAG, "cursor position stream attached"); + return self; + } + + private void readLoop() { + try (InputStream in = new ParcelFileDescriptor.AutoCloseInputStream(readSide)) { + var din = new DataInputStream(in); + byte[] buf = new byte[8]; + while (!closed) { + // readFully, not read: the writer emits 8-byte records and a short read would + // desynchronise the stream permanently, turning every later position into noise. + din.readFully(buf); + int x = le32(buf, 0); + int y = le32(buf, 4); + mainHandler.post(() -> { + if (!closed) { + listener.onCursorMoved(x, y); + } + }); + } + } catch (IOException e) { + if (!closed) { + // Normal at VM shutdown: crosvm exits and the write end closes. + } + } + } + + private static int le32(byte[] b, int off) { + return (b[off] & 0xFF) + | ((b[off + 1] & 0xFF) << 8) + | ((b[off + 2] & 0xFF) << 16) + | ((b[off + 3] & 0xFF) << 24); + } + + @Override + public void close() { + closed = true; + closeQuietly(readSide); // unblocks readFully + readSide = null; + } + + private static void closeQuietly(ParcelFileDescriptor pfd) { + if (pfd == null) return; + try { + pfd.close(); + } catch (IOException ignored) { + // Nothing useful to do; the fd is going away either way. + } + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/display/DisplayProvider.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/display/DisplayProvider.java index f5e85ea4e..3e068ec13 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/display/DisplayProvider.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/display/DisplayProvider.java @@ -20,6 +20,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; import java.util.function.Supplier; @@ -35,9 +36,30 @@ */ final class DisplayProvider { private static final String TAG = "NativeDisplayProvider"; - // Stop polling for the display binder after this many 1s rounds (the daemon-side - // waitForService already blocks up to 5s each), so a dead broker doesn't spin forever. - private static final int MAX_BINDER_ATTEMPTS = 30; + // Keep looking for the display binder for as long as this console is open, backing off from + // BINDER_RETRY_MIN_MS to BINDER_RETRY_MAX_MS between rounds. + // + // There used to be a cap of 30 rounds, because each round could cost the daemon a leaked, + // permanently-blocked thread (see NativeDisplayBinder) and spinning was genuinely expensive. + // It no longer is -- a round is one cheap ServiceManager lookup, answered instantly when the + // VM is not running -- and a cap is the wrong shape anyway: the thing being waited for is a + // guest booting, which has no upper bound. A guest that took longer than the cap left the + // console blank for the rest of its life with nothing left to retry it. + // + // Two things end the wait instead of a counter: the binder turning up, and the console + // closing (shutdown() interrupts this thread). A VM that exits while the console is open + // closes the console itself, so "the VM is never coming back" ends it too. + private static final long BINDER_RETRY_MIN_MS = 1000; + private static final long BINDER_RETRY_MAX_MS = 5000; + // Interval for re-reading the guest display size. crosvm's binder is pull-only (no resize + // push callback, and SELinux blocks crosvm from calling back into an untrusted_app), so the + // receiver polls the single source of truth -- getDisplayConfig() reflects the current scanout + // size, updated by C++ configure() on EVERY resolution change (UEFI modeset, Linux boot, and + // Linux runtime xrandr all flow through it). This is what lets the aspect ratio follow a guest + // resolution change mid-session. A resize is rare and user-driven, so 1s lag is fine. + private static final long CONFIG_POLL_MS = 1000; + /** virtio-gpu's cursor plane size; a smaller cursor image lands in its top-left. */ + static final int CURSOR_PLANE_PX = 64; private final SurfaceView mainView; private int width; @@ -54,8 +76,15 @@ final class DisplayProvider { private ICrosvmAndroidDisplayService displayService; private boolean needsSend = false; private boolean hasSavedFrame = false; + private boolean hasSavedCursorFrame = false; + private CursorPositionStream cursorStream; + private CursorPositionStream.Listener cursorListener; + private SurfaceView cursorView; + private boolean cursorSurfaceSent = false; private final IBinder.DeathRecipient deathRecipient; + /** One binder hunt at a time: both the death path and surfaceCreated ask for one. */ + private final AtomicBoolean fetching = new AtomicBoolean(); DisplayProvider(@NonNull SurfaceView mainView, int width, int height, @NonNull Supplier binderProvider, @@ -71,7 +100,36 @@ final class DisplayProvider { Log.w(TAG, "display service died - connection lost"); onConnected.accept(false); displayService = null; + // The display service lives in crosvm. When the guest reboots (or crosvm otherwise + // restarts) the daemon relaunches crosvm, which registers a NEW display service under + // the same name -- but nothing re-fetches it, so the activity sat on "waiting for the + // VM screen" until the user closed and reopened it (seen on every provisioning reboot). + // Re-arm: the surface we hold is still valid, so mark it for re-delivery and poll for + // the new binder the same way the first attach did. shutdown() has already stopped + // the executor when the activity is going away, so a real teardown does not re-poll. + if (executor.isShutdown()) { + needsSend = false; + return; + } needsSend = false; + cursorSurfaceSent = false; + hasSavedFrame = false; + hasSavedCursorFrame = false; + if (cursorStream != null) { + cursorStream.close(); + cursorStream = null; + } + // Do NOT hand the old Surface to the new crosvm. Its BufferQueue still carries the + // state the dead producer left behind (buffers dequeued and never returned): the CPU + // console path can still post to it, but the GPU-blit scanout path silently stops -- + // observed as the early-boot console frozen on screen with a live cursor. Closing and + // reopening the activity fixed it because that creates fresh surfaces, so do exactly + // that here: bounce both SurfaceViews so surfaceDestroyed/surfaceCreated run and the + // usual send-once-per-surface path delivers brand-new surfaces once the binder is back. + recreateSurface(mainView); + if (cursorView != null) recreateSurface(cursorView); + Log.i(TAG, "re-fetching the display binder for the restarted VM (fresh surfaces)"); + fetchBinder(); }); mainView.setSurfaceLifecycle(SurfaceView.SURFACE_LIFECYCLE_FOLLOWS_ATTACHMENT); @@ -85,35 +143,56 @@ final class DisplayProvider { fetchBinder(); } + private void recreateSurface(@NonNull SurfaceView view) { + var parent = view.getParent(); + if (!(parent instanceof android.view.ViewGroup)) { + Log.w(TAG, "recreateSurface: view has no ViewGroup parent"); + return; + } + var group = (android.view.ViewGroup) parent; + int idx = group.indexOfChild(view); + var lp = view.getLayoutParams(); + Log.i(TAG, fmt("recreateSurface: detaching and re-attaching %s", + view.getClass().getSimpleName())); + group.removeView(view); + group.addView(view, idx, lp); + } + private void fetchBinder() { + if (!fetching.compareAndSet(false, true)) return; executor.submit(() -> { - IBinder binder = null; - int attempts = 0; - while (!Thread.currentThread().isInterrupted() && attempts < MAX_BINDER_ATTEMPTS) { - try { - binder = binderProvider.get(); - } catch (Exception e) { - Log.e(TAG, "binderProvider threw", e); - binder = null; - } - if (binder != null) break; - attempts++; - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - return; + try { + long backoff = BINDER_RETRY_MIN_MS; + int rounds = 0; + while (!Thread.currentThread().isInterrupted()) { + IBinder binder; + try { + binder = binderProvider.get(); + } catch (Exception e) { + Log.e(TAG, "binderProvider threw", e); + binder = null; + } + if (binder != null) { + final IBinder got = binder; + mainHandler.post(() -> onBinderReady(got)); + return; + } + // One line early and then one a minute: a slow guest is normal and must not + // be the reason a log is unreadable. + if (rounds == 0 || rounds % 60 == 0) + Log.i(TAG, fmt("display binder not there yet (round %d)", rounds + 1)); + rounds++; + try { + Thread.sleep(backoff); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + backoff = Math.min(backoff * 2, BINDER_RETRY_MAX_MS); } + } finally { + fetching.set(false); } - final IBinder got = binder; - if (got == null) { - // Give up rather than poll forever: the daemon broker may have died (the supplier - // returns null once rootService is dropped), so the display can't be reached. - Log.e(TAG, fmt("display binder unavailable after %d attempts", MAX_BINDER_ATTEMPTS)); - mainHandler.post(() -> onConnected.accept(false)); - return; - } - mainHandler.post(() -> onBinderReady(got)); }); } @@ -136,8 +215,53 @@ private void onBinderReady(@NonNull IBinder binder) { } catch (Exception e) { Log.w(TAG, fmt("getDisplayConfig unavailable, using default %dx%d", width, height)); } + // Attach the cursor position pipe, if anybody wants it. crosvm has always written guest + // cursor moves to this fd; nothing ever read it, so the backend dropped them all. + attachCursorStream(); + if (cursorView != null) { + trySendCursorSurface(cursorView.getHolder()); + } + onConnected.accept(true); applyPendingSurface(); + startConfigPoll(displayService); + } + + // Watches for guest resolution changes: crosvm has no push channel, so re-read the current + // scanout size on the bg thread and, when it changes, re-lay-out the surface. Runs on the + // single background executor; a getDisplayConfig() failure (dead service) ends the loop. + private void startConfigPoll(@NonNull ICrosvmAndroidDisplayService svc) { + executor.submit(() -> { + while (!Thread.currentThread().isInterrupted()) { + try { + Thread.sleep(CONFIG_POLL_MS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + DisplayConfig config; + try { + config = svc.getDisplayConfig(); + } catch (Exception e) { + return; // service gone; deathRecipient handles teardown + } + if (config == null || config.width <= 0 || config.height <= 0) continue; + mainHandler.post(() -> applyConfigIfChanged(config)); + } + }); + } + + // Main thread: apply a newly observed guest size. width/height stay main-thread-only. + private void applyConfigIfChanged(@NonNull DisplayConfig config) { + if (config.width == width && config.height == height) return; + Log.i(TAG, fmt("guest display size changed %dx%d -> %dx%d", + width, height, config.width, config.height)); + width = config.width; + height = config.height; + // Keep the app-side buffer size in step with crosvm's setBuffersGeometry; guarded by + // needsSend, so the layout-driven surfaceChanged this triggers won't re-send the surface. + mainView.getHolder().setFixedSize(width, height); + onDisplayConfig.accept(config); } private void applyPendingSurface() { @@ -215,6 +339,115 @@ public void surfaceDestroyed(@NonNull SurfaceHolder holder) { } } + /** + * Ask for guest cursor positions. Safe to call before or after the display binder arrives: + * whichever happens second does the attaching, so a caller does not have to know which. + */ + void setCursorListener(CursorPositionStream.Listener listener) { + cursorListener = listener; + attachCursorStream(); + } + + private void attachCursorStream() { + if (cursorListener == null || displayService == null || cursorStream != null) { + return; + } + cursorStream = CursorPositionStream.attach(displayService, cursorListener); + } + + /** + * Give crosvm a Surface for the guest's HARDWARE cursor. + * + * Until something calls setSurface(_, forCursor=true), the native backend's cursor surface has + * no native window: lock() hands crosvm a sink buffer whose own comment says it "is never + * displayed on the physical screen", and unlockAndPost() returns without posting. The pointer + * is rendered perfectly and thrown away. + * + * Linux guests can be told to draw the pointer into the framebuffer instead, which is why this + * went unnoticed; Windows' virtio-gpu driver and UEFI have no equivalent and use the cursor + * plane unconditionally, so without this they have no visible pointer at all. + */ + void setCursorView(SurfaceView view) { + cursorView = view; + if (view == null) { + return; + } + // Above the scanout SurfaceView. Both are surfaces in their own layers, so ordinary view + // z-order does not apply -- without this the cursor is composited BEHIND the display. + view.setZOrderMediaOverlay(true); + // Same rule as the scanout: this Surface must outlive a visibility change. The guest hides + // and re-shows its pointer constantly -- KDE drops to a software cursor whenever the + // hardware plane cannot keep up with a fast move and hands the plane straight back -- and + // the image comes back in ONE UPDATE_CURSOR, whose pixels are flushed microseconds after + // the position that tells the app to show the overlay again. Re-creating a destroyed + // Surface takes a frame plus a binder round trip, so those pixels land in the backend's + // sink buffer and are dropped, and every later MOVE_CURSOR carries a position and no image + // -- the pointer stays blank until the guest happens to change its shape. The activity + // therefore parks the overlay off-screen rather than setting it GONE; on Android 14+ this + // makes the Surface immune to visibility for good measure. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + view.setSurfaceLifecycle(SurfaceView.SURFACE_LIFECYCLE_FOLLOWS_ATTACHMENT); + } + view.getHolder().setFormat(android.graphics.PixelFormat.TRANSLUCENT); + view.getHolder().setFixedSize(CURSOR_PLANE_PX, CURSOR_PLANE_PX); + view.getHolder().addCallback(new SurfaceHolder.Callback() { + @Override public void surfaceCreated(@NonNull SurfaceHolder h) { + cursorSurfaceSent = false; + trySendCursorSurface(h); + } + @Override public void surfaceChanged(@NonNull SurfaceHolder h, int f, int w, int hh) { + // Same rule as the main surface: setSurface at most once per surface lifetime. + trySendCursorSurface(h); + } + @Override public void surfaceDestroyed(@NonNull SurfaceHolder h) { + cursorSurfaceSent = false; + var svc = displayService; + if (svc == null) return; + // Keep the last pointer image the same way the scanout keeps its last frame. A + // cursor surface that really did die (the activity left the foreground, or the + // death path bounced it) comes back empty otherwise, and nothing repaints it: only + // UPDATE_CURSOR carries pixels, and a pointer that merely moves sends MOVE_CURSOR. + try { + svc.saveFrameForSurface(true); + hasSavedCursorFrame = true; + } catch (Exception e) { + Log.w(TAG, "saveFrameForSurface(cursor) failed", e); + } + try { + svc.removeSurface(true); + } catch (Exception e) { + Log.w(TAG, "removeSurface(cursor) failed", e); + } + } + }); + trySendCursorSurface(view.getHolder()); + } + + private void trySendCursorSurface(@NonNull SurfaceHolder holder) { + var svc = displayService; + if (svc == null || cursorSurfaceSent) return; + Surface surface = holder.getSurface(); + if (surface == null || !surface.isValid()) return; + try { + svc.setSurface(surface, true); + cursorSurfaceSent = true; + Log.i(TAG, "cursor surface delivered"); + } catch (IllegalArgumentException e) { + // Same known/benign binder reply behaviour as the main surface. + cursorSurfaceSent = true; + } catch (Exception e) { + Log.w(TAG, "setSurface(cursor) failed", e); + return; + } + if (hasSavedCursorFrame) { + try { + svc.drawSavedFrameForSurface(true); + } catch (Exception e) { + Log.w(TAG, "drawSavedFrameForSurface(cursor) failed", e); + } + } + } + void shutdown() { if (displayService != null) { try { @@ -222,6 +455,10 @@ void shutdown() { } catch (Exception ignored) { } } + if (cursorStream != null) { + cursorStream.close(); + cursorStream = null; + } executor.shutdownNow(); displayService = null; needsSend = false; diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/display/NativeSurfaceSource.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/display/NativeSurfaceSource.java new file mode 100644 index 000000000..6a839f1b5 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/display/NativeSurfaceSource.java @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. +package cn.classfun.droidvm.ui.vm.display.nativedisplay.display; + +import android.os.Handler; +import android.os.IBinder; +import android.view.SurfaceView; + +import androidx.annotation.NonNull; + +import java.util.function.Supplier; + +import cn.classfun.droidvm.ui.vm.display.base.DisplaySource; + +/** + * {@link DisplaySource} for the native display path: crosvm renders straight into the + * SurfaceView's Surface via the per-VM ICrosvmAndroidDisplayService. Wraps {@link DisplayProvider} + * behind the pluggable source interface, so a future source (e.g. zero-copy AHardwareBuffer) + * slots into the same console. Frames start flowing as soon as the display-binder supplier can + * resolve the service, so {@link #start()} is a no-op. No guest-resize channel on this path yet. + */ +public final class NativeSurfaceSource implements DisplaySource { + private final DisplayProvider provider; + + /** + * @param displayBinderSupplier resolves the per-VM display binder (blocking, called off the + * main thread); null while the daemon broker isn't attached yet. + */ + public NativeSurfaceSource(@NonNull SurfaceView surfaceView, int guestWidth, int guestHeight, + @NonNull Supplier displayBinderSupplier, + @NonNull Handler mainHandler, @NonNull Callbacks callbacks) { + provider = new DisplayProvider(surfaceView, guestWidth, guestHeight, displayBinderSupplier, + connected -> mainHandler.post(() -> callbacks.onStateChanged( + connected ? State.CONNECTED : State.CONNECTING)), + config -> mainHandler.post(() -> callbacks.onContentSize(config.width, config.height))); + } + + /** + * Follow the guest cursor. Positions are GUEST framebuffer coordinates, delivered on the main + * thread, and arrive only on this display path -- the VNC path has no such channel. + * + * The caller decides what to do with them, and in particular whether to act at all: a cursor + * move caused by a remote desktop session is indistinguishable here from one the user made, + * so gating on "the user is currently driving the pointer" belongs to the consumer. + */ + @Override + public void setCursorView(android.view.SurfaceView cursorView) { + provider.setCursorView(cursorView); + } + + @Override + public void setCursorListener(java.util.function.BiConsumer listener) { + provider.setCursorListener(listener == null ? null : listener::accept); + } + + @Override + public void start() { + } + + @Override + public void shutdown() { + provider.shutdown(); + } + + @Override + public boolean supportsGuestResize() { + return false; + } + + @Override + public void requestGuestResize(int width, int height) { + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/display/VMNativeDisplayActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/display/VMNativeDisplayActivity.java index a6209ade8..2e9c70b6e 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/display/VMNativeDisplayActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/display/VMNativeDisplayActivity.java @@ -10,16 +10,12 @@ import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; import android.annotation.SuppressLint; -import android.content.BroadcastReceiver; import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; +import android.graphics.RectF; import android.graphics.drawable.GradientDrawable; import android.os.Bundle; import android.os.Handler; -import android.os.IBinder; import android.os.Looper; -import android.os.RemoteException; import android.util.Base64; import android.util.Log; import android.view.KeyCharacterMap; @@ -38,6 +34,10 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; +import androidx.core.graphics.Insets; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowCompat; +import androidx.core.view.WindowInsetsCompat; import com.google.android.material.appbar.MaterialToolbar; import com.google.android.material.button.MaterialButton; @@ -49,13 +49,27 @@ import cn.classfun.droidvm.R; import cn.classfun.droidvm.display.INativeDisplayRootService; +import cn.classfun.droidvm.DroidVMApp; import cn.classfun.droidvm.lib.daemon.DaemonConnection; +import cn.classfun.droidvm.lib.daemon.ForegroundCallback; import cn.classfun.droidvm.lib.store.vm.NativeDisplay; import cn.classfun.droidvm.lib.ui.DragTouchListener; +import cn.classfun.droidvm.lib.ui.ImeInsetsExempt; import cn.classfun.droidvm.lib.ui.MaterialMenu; +import cn.classfun.droidvm.ui.vm.display.base.DaemonDisplayAttach; +import cn.classfun.droidvm.ui.vm.display.base.DisplayChromeController; import cn.classfun.droidvm.ui.vm.display.base.DisplayExtraKeysPanel; +import cn.classfun.droidvm.ui.vm.display.base.DisplayKeyboardMenuRow; +import cn.classfun.droidvm.ui.vm.display.base.KeyboardMode; +import cn.classfun.droidvm.ui.vm.display.base.DisplayPhysicalKeyboardView; +import cn.classfun.droidvm.ui.vm.display.base.DisplaySource; +import cn.classfun.droidvm.ui.vm.display.base.DisplayViewportController; +import cn.classfun.droidvm.ui.vm.display.base.InputMode; +import cn.classfun.droidvm.ui.vm.display.base.PointerGestureTranslator; +import cn.classfun.droidvm.ui.vm.display.nativedisplay.input.EvdevEncoder; import cn.classfun.droidvm.ui.vm.display.nativedisplay.input.DirectInputSink; import cn.classfun.droidvm.ui.vm.display.nativedisplay.input.InputForwarder; +import cn.classfun.droidvm.lib.perf.SystemGestureGuard; import cn.classfun.droidvm.ui.vm.display.nativedisplay.input.NativeExtraKeysPanel; import cn.classfun.droidvm.ui.vm.display.nativedisplay.input.NativeKeyboardEditText; import cn.classfun.droidvm.ui.vm.display.nativedisplay.input.TouchScaleCalculator; @@ -72,13 +86,15 @@ * The binder can't ride the daemon's TCP/JSON-RPC channel, so it arrives via a broadcast the daemon * sends in response to the {@code display_attach} request below. */ -public final class VMNativeDisplayActivity extends AppCompatActivity { +// ImeInsetsExempt: the display area handles the IME inset itself (root insets listener below); +// without the exemption the app-wide ImeInsetsApplier would pad the content view a second time. +public final class VMNativeDisplayActivity extends AppCompatActivity + implements ImeInsetsExempt, ForegroundCallback { private static final String TAG = "VMNativeDisplay"; public static final String EXTRA_VM_NAME = "vm_name"; public static final String EXTRA_VM_ID = "vm_id"; public static final String EXTRA_WIDTH = "display_width"; public static final String EXTRA_HEIGHT = "display_height"; - private static final long AUTO_HIDE_DELAY_MS = 3000; private final Handler mainHandler = new Handler(Looper.getMainLooper()); // Converts committed IME text into key events (handles Shift for upper-case/symbols). @@ -93,10 +109,24 @@ public final class VMNativeDisplayActivity extends AppCompatActivity { private TextView tvConnectingMessage; private FrameLayout displayContainer; private SurfaceView surfaceView; + private SurfaceView cursorView; + + // Last viewport transform, mirrored so the cursor overlay can be placed without asking the + // controller to recompute it. Written only by onViewportChanged, read only on the main thread. + private float vpBaseW, vpBaseH, vpViewScale = 1f, vpOffsetX, vpOffsetY; + + // True while a finger is on the container in relative-pointer mode. This is the gate for + // viewport follow: a cursor move from a REMOTE DESKTOP session is byte-identical to one the + // user made -- the position stream cannot tell them apart -- so only the input layer, which + // knows whether this device is currently driving, may decide to pan. + private boolean pointerDriveActive = false; + private int lastCursorX = -1; + private int lastCursorY = -1; private NativeKeyboardEditText keyboardInput; private FloatingActionButton fabMenu; private MaterialButton btnFullscreen; private DisplayExtraKeysPanel extraKeysPanel; + private DisplayPhysicalKeyboardView phyKeyboard; private String vmName = ""; private String vmId = ""; @@ -104,37 +134,116 @@ public final class VMNativeDisplayActivity extends AppCompatActivity { private int guestWidth = 1280; private int guestHeight = 720; - private INativeDisplayRootService rootService; - private DisplayProvider displayProvider; + private DisplaySource displaySource; private InputForwarder inputForwarder; private DirectInputSink directSink; private NativeExtraKeysPanel nativeExtraKeys; - private boolean isFullscreen = false; private boolean connected = false; + // Pointer input mode, shared with the VNC path via the "display_input_mode" pref; applied to the + // InputForwarder so on-screen touches route to the multi-touch / mouse / tablet virtio device. + private InputMode inputMode = InputMode.TOUCH; + private static final String INPUT_PREFS = "droidvm_prefs"; + private static final String KEY_INPUT_MODE = "display_input_mode"; + // Chrome memory, shared with the VNC path: extra-keys on/off per typing surface + whether + // the physical keyboard is up. + private static final String KEY_KEYBOARD_MODE = "display_keyboard_mode"; + private static final String KEY_ZONE_EXTRA = "display_keyboard_zone_extra"; + private static final String KEY_ZONE_FNX = "display_keyboard_zone_fnx"; + // Unified MOUSE/TABLET gesture layer (two-finger tap = right click, two-finger pan = scroll, + // three-finger = local zoom/pan). TOUCH mode bypasses it and stays raw multi-touch. + private PointerGestureTranslator gestureTranslator; + // Fractional remainders of relative mouse motion so slow drags aren't rounded away. + private float mouseRemX, mouseRemY; + // Last mouse right/middle-button activity: any BACK arriving shortly after is the framework's + // (or OEM's) right-click fallback, regardless of what source it claims - swallow it. + private long lastMouseButtonMs; + private static final long MOUSE_BACK_SUPPRESS_MS = 800; + // Single sources of truth for viewport geometry (fit/zoom/pan across display-area changes) + // and chrome visibility (fullscreen / extra keys). See the controller classes for the rules. + private DisplayViewportController viewport; + private DisplayChromeController chrome; + // Display areas smaller than this (e.g. landscape with a tall IME) freeze the viewport + // instead of re-laying it out; see DisplayViewportController. + private static final int MIN_AREA_DP = 96; + /** Screen-space breathing room kept around the guest cursor when the view follows it. */ + private static final float CURSOR_FOLLOW_MARGIN_PX = 96f; + /** + * Where the cursor overlay goes while the guest's pointer is hidden. + * + * NOT setVisibility(GONE). A SurfaceView's Surface follows its visibility, so GONE destroys it + * and the app hands crosvm a removeSurface(cursor). The guest hides and re-shows the pointer + * all the time -- KDE drops to a software cursor whenever the hardware plane cannot keep up + * with a fast move, then hands the plane straight back -- and the image comes back in ONE + * UPDATE_CURSOR, which writes the position down the pipe and flushes the pixels in the same + * breath. Re-creating a Surface takes a frame plus a binder round trip, so those pixels land + * in the backend's sink buffer and are dropped; every later MOVE_CURSOR carries a position and + * no image, leaving the pointer blank until the guest happens to change its shape. Parking the + * view keeps the Surface alive, so the flush has somewhere to land. Far enough off-screen to + * be outside any panel, small enough to stay well clear of float/int overflow in the layer. + */ + private static final float CURSOR_PARKED_PX = -10000f; - // Per-attach random token: we only accept the binder broadcast carrying the nonce we requested, - // so another app spoofing the (exported) action can't slip us a fake broker binder. - private final String attachNonce = UUID.randomUUID().toString(); - private boolean binderReceiverRegistered = false; - private boolean rootConnected = false; - - // The daemon broadcasts its broker binder here in response to our display_attach request. - private final BroadcastReceiver binderReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - var bundle = intent.getBundleExtra(NativeDisplay.EXTRA_BUNDLE); - if (bundle == null || !attachNonce.equals(bundle.getString(NativeDisplay.EXTRA_NONCE))) - return; - var binder = bundle.getBinder(NativeDisplay.EXTRA_BINDER); - if (binder != null) onBinderReceived(binder); - } - }; + // Maps the unified gestures onto the crosvm --input evdev channels via InputForwarder. + private final PointerGestureTranslator.Listener gestureListener = + new PointerGestureTranslator.Listener() { + @Override + public void onRelativeMove(float dxGuest, float dyGuest) { + if (inputForwarder == null) return; + mouseRemX += dxGuest; + mouseRemY += dyGuest; + int dx = (int) mouseRemX, dy = (int) mouseRemY; + if (dx == 0 && dy == 0) return; + mouseRemX -= dx; + mouseRemY -= dy; + inputForwarder.sendMouseMove(dx, dy); + } + + @Override + public void onAbsoluteMove(float xGuest, float yGuest) { + if (inputForwarder != null) + inputForwarder.sendAbsMove(Math.round(xGuest), Math.round(yGuest)); + } + + @Override + public void onLeftButton(boolean down, float xGuest, float yGuest) { + if (inputForwarder == null) return; + if (inputMode == InputMode.TABLET) { + inputForwarder.sendAbsLeftButton(down, Math.round(xGuest), Math.round(yGuest)); + } else { + inputForwarder.sendPointerButton(EvdevEncoder.BTN_LEFT, down); + } + } + + @Override + public void onLeftTap(float xGuest, float yGuest) { + onLeftButton(true, xGuest, yGuest); + onLeftButton(false, xGuest, yGuest); + } + + @Override + public void onRightClick(float xGuest, float yGuest) { + if (inputForwarder == null) return; + if (inputMode == InputMode.TABLET) + inputForwarder.sendAbsMove(Math.round(xGuest), Math.round(yGuest)); + inputForwarder.sendPointerButton(EvdevEncoder.BTN_RIGHT, true); + inputForwarder.sendPointerButton(EvdevEncoder.BTN_RIGHT, false); + } + + @Override + public void onScroll(int vNotches, int hNotches) { + if (inputForwarder != null) inputForwarder.sendScroll(vNotches, hNotches); + } + + @Override + public void onZoomPan(float scaleFactor, float dxView, float dyView, + float focusX, float focusY) { + if (viewport != null) viewport.onZoomPan(scaleFactor, dxView, dyView); + } + }; - // Daemon died (e.g. restart): drop the broker binder so writes fall back to the vm_input IPC. - private final IBinder.DeathRecipient deathRecipient = () -> mainHandler.post(() -> { - Log.w(TAG, "daemon broker binder died"); - rootService = null; - }); + // Daemon broker binder acquisition (display_attach -> nonce-matched broadcast), shared with + // the VNC display path. + private DaemonDisplayAttach displayAttach; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { @@ -153,6 +262,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { toolbar.setTitle(vmName.isEmpty() ? getString(R.string.native_display_title) : vmName); toolbar.setNavigationOnClickListener(v -> finish()); setupViews(); + setupLayoutControllers(); setStatus(getString(R.string.native_display_connecting), R.color.vnc_status_connecting); showOverlay(getString(R.string.native_display_waiting)); @@ -161,52 +271,29 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { showOverlay(getString(R.string.native_display_failed)); return; } - // Ask the daemon (uid=0) for its broker binder. It can't ride the JSON-RPC channel, so the - // daemon broadcasts it back to binderReceiver (matched by attachNonce). - registerReceiver(binderReceiver, - new IntentFilter(NativeDisplay.BINDER_BROADCAST_ACTION), Context.RECEIVER_EXPORTED); - binderReceiverRegistered = true; - requestDisplayBinder(10); - } - - // Sends display_attach; the daemon answers with a broadcast. Retries while the daemon connection - // is still coming up, since the Activity can open just before DaemonConnection authenticates. - private void requestDisplayBinder(int attemptsLeft) { - if (rootConnected || isFinishing()) return; - DaemonConnection.getInstance().buildRequest("display_attach") - .put("nonce", attachNonce) - .onError(e -> retryDisplayBinder(attemptsLeft)) - .onUnsuccessful(r -> retryDisplayBinder(attemptsLeft)) - .invoke(); - } - - private void retryDisplayBinder(int attemptsLeft) { - if (attemptsLeft <= 0) { - Log.w(TAG, "display_attach exhausted retries; daemon unavailable"); - return; - } - mainHandler.postDelayed(() -> requestDisplayBinder(attemptsLeft - 1), 500); - } + displayAttach = new DaemonDisplayAttach(this, mainHandler, + new DaemonDisplayAttach.Listener() { + @Override + public void onAttached(@NonNull INativeDisplayRootService service) { + onRootConnected(service); + } - private void onBinderReceived(@NonNull IBinder binder) { - if (rootConnected) return; // ignore duplicate broadcasts - rootConnected = true; - rootService = INativeDisplayRootService.Stub.asInterface(binder); - try { - binder.linkToDeath(deathRecipient, 0); - } catch (RemoteException e) { - Log.w(TAG, "linkToDeath failed", e); - } - onRootConnected(); + @Override + public void onLost() { + // DirectInputSink falls back to the vm_input RPC per write on a dead binder. + } + }); + displayAttach.start(); } - private void onRootConnected() { - if (rootService == null) return; + private void onRootConnected(@NonNull INativeDisplayRootService service) { // Try a direct unix-socket sink to the daemon (one write per evdev frame, no IPC // round-trip); on any failure it falls back to the vm_input JSON-RPC path below. - directSink = new DirectInputSink(vmId, rootService, this::sendInputToDaemon); inputForwarder = new InputForwarder(directSink); if (nativeExtraKeys != null) nativeExtraKeys.setForwarder(inputForwarder); + // A forwarder is built fresh on every attach and starts in TOUCH, so it has to be told + // the mode this session is actually in (restored in setupViews, or since changed). + inputForwarder.setInputMode(inputMode); // Start the VM now that listeners are up (no-op if already running). DaemonConnection.getInstance().buildRequest("vm_start") @@ -217,10 +304,10 @@ private void onRootConnected() { .invoke(); // Display binder is looked up via the root service (servicemanager) on a bg thread. - displayProvider = new DisplayProvider( + displaySource = new NativeSurfaceSource( surfaceView, guestWidth, guestHeight, () -> { - var svc = rootService; + var svc = displayAttach.getService(); if (svc == null) return null; try { return svc.waitForDisplayBinder(vmKey); @@ -228,22 +315,40 @@ private void onRootConnected() { return null; } }, - isConnected -> mainHandler.post(() -> onDisplayConnected(isConnected)), - config -> mainHandler.post(() -> { - guestWidth = config.width; - guestHeight = config.height; - updateAspectRatio(displayContainer.getWidth(), displayContainer.getHeight()); - }) - ); - } - - private void onDisplayConnected(boolean isConnected) { - connected = isConnected; - if (isConnected) { + mainHandler, + new DisplaySource.Callbacks() { + @Override + public void onContentSize(int width, int height) { + guestWidth = width; + guestHeight = height; + viewport.setContentSize(width, height); + // Keep the status-bar resolution label in step with a live resize. + if (connected) + setStatus(fmt(getString(R.string.native_display_connected), + guestWidth, guestHeight), R.color.vnc_status_connected); + } + + @Override + public void onStateChanged(@NonNull DisplaySource.State state) { + onDisplayStateChanged(state); + } + }); + // Hardware cursor: give crosvm a Surface for the guest's cursor plane and follow the + // positions it reports. Both are no-ops on a guest that never uses the cursor plane. + // This must run here, on the main thread with displaySource assigned -- it used to sit + // inside the binder-supplier lambda above, where the first invocation raced the field + // assignment on a background thread and the cursor layer was wired only by luck. + displaySource.setCursorView(cursorView); + displaySource.setCursorListener(this::onGuestCursorMoved); + displaySource.start(); + } + + private void onDisplayStateChanged(@NonNull DisplaySource.State state) { + connected = state == DisplaySource.State.CONNECTED; + if (connected) { setStatus(fmt(getString(R.string.native_display_connected), guestWidth, guestHeight), R.color.vnc_status_connected); hideOverlay(); - updateAspectRatio(displayContainer.getWidth(), displayContainer.getHeight()); } else { setStatus(getString(R.string.native_display_connecting), R.color.vnc_status_connecting); showOverlay(getString(R.string.native_display_waiting)); @@ -258,24 +363,91 @@ private void bindViews() { overlayConnecting = findViewById(R.id.overlay_connecting); tvConnectingMessage = findViewById(R.id.tv_connecting_message); displayContainer = findViewById(R.id.display_container); + cursorView = findViewById(R.id.cursor_view); + // Visible from the start, parked off-screen: the Surface exists before the guest's first + // UPDATE_CURSOR, which is the only message that carries pointer pixels. Laid out GONE, the + // first pointer image was flushed into a Surface that did not exist yet and the pointer + // stayed blank until the next shape change -- the same hole the hide path used to open. + cursorView.setVisibility(VISIBLE); + parkCursorOverlay(); surfaceView = findViewById(R.id.surface_view); keyboardInput = findViewById(R.id.keyboard_input); fabMenu = findViewById(R.id.fab_menu); btnFullscreen = findViewById(R.id.btn_fullscreen); extraKeysPanel = findViewById(R.id.extra_keys_panel); nativeExtraKeys = new NativeExtraKeysPanel(extraKeysPanel); + phyKeyboard = findViewById(R.id.phy_keyboard); + phyKeyboard.setKeyListener(nativeExtraKeys); + // The physical keyboard's Shift/Ctrl/Alt/Win mirror the panel's sticky-modifier state. + extraKeysPanel.setModifierStateObserver(() -> phyKeyboard.refreshModifiers( + extraKeysPanel.isCtrlDown(), extraKeysPanel.isAltDown(), + extraKeysPanel.isShiftDown(), extraKeysPanel.isWinDown())); + extraKeysPanel.setZoneListener(new DisplayExtraKeysPanel.ZoneListener() { + @Override + public void onToggleFnxZone() { + chrome.toggleFnxZone(); + } + + @Override + public void onShowSystemKeyboard() { + toggleSoftKeyboard(); + } + }); + phyKeyboard.setZoneListener(new DisplayPhysicalKeyboardView.ZoneListener() { + @Override + public void onToggleExtraZone() { + chrome.toggleExtraZone(); + } + + @Override + public void onToggleFnxZone() { + chrome.toggleFnxZone(); + } + + @Override + public void onCloseKeyboard() { + chrome.setKeyboardMode(KeyboardMode.NONE); + } + }); } @SuppressLint("ClickableViewAccessibility") private void setupViews() { btnFullscreen.setOnClickListener(v -> toggleFullscreen()); + // The container's layout size IS the display area: chrome visibility, IME and rotation + // all funnel into it through normal layout. The viewport handles degenerate sizes itself. displayContainer.addOnLayoutChangeListener(( v, l, t, r, b, ol, ot, or2, ob ) -> { int cw = r - l, ch = b - t; - if (cw > 0 && ch > 0) v.post(() -> updateAspectRatio(cw, ch)); + v.post(() -> viewport.setArea(cw, ch)); }); surfaceView.setOnTouchListener(this::onSurfaceTouch); + // MOUSE/TABLET gestures live on the container: the whole display area (letterbox included) + // is gesture surface; the translator pins coordinate ops to the rendered surface rect. + // In TOUCH mode this listener declines and raw multi-touch stays on the surface view. + displayContainer.setOnTouchListener(this::onContainerTouch); + // Host mouse/stylus: scroll wheel + right/middle buttons come as generic-motion events, + // hover comes as hover events; both feed the pointer device so right-click/scroll/hover + // pass through to the guest (tablet mode gives absolute hover). + surfaceView.setOnGenericMotionListener(this::onSurfaceGenericMotion); + // Also on the container: a right-click over the letterbox area (outside the surface) must + // still be consumed or the framework synthesizes BACK from it. + displayContainer.setOnGenericMotionListener(this::onSurfaceGenericMotion); + surfaceView.setOnHoverListener(this::onSurfaceHover); + // Restore the persisted mode here rather than on the daemon attach: the FAB menu is + // reachable before the binder arrives, and a menu built on a stale TOUCH would write that + // back over the stored mode. + inputMode = InputMode.fromOrdinal( + getSharedPreferences(INPUT_PREFS, MODE_PRIVATE).getInt(KEY_INPUT_MODE, 0)); + gestureTranslator = new PointerGestureTranslator(mainHandler, gestureListener); + gestureTranslator.setAbsolute(inputMode == InputMode.TABLET); + // Keep the display area out of the system-gesture zones so multi-finger gestures + // (two-finger right-click/scroll, three-finger zoom) don't trip OEM gestures like + // three-finger screenshot or edge-back. + displayContainer.addOnLayoutChangeListener((v, l, t, r, b, ol, ot, or2, ob) -> + v.setSystemGestureExclusionRects( + java.util.Collections.singletonList(new android.graphics.Rect(0, 0, r - l, b - t)))); keyboardInput.setTextInputListener(new NativeKeyboardEditText.TextInputListener() { @Override public void onCommitText(@NonNull CharSequence text) { @@ -293,18 +465,26 @@ public void onDeleteSurrounding(int beforeLength, int afterLength) { } // Soft keyboards that commit text (instead of sending key events) land here; translate each - // character to its key event sequence and forward as evdev. + // character to its evdev key sequence and forward it. Each char is resolved on its own so one + // unrepresentable character can't drop the whole commit. Uppercase letters and shifted symbols + // go through the deterministic US-layout table (Shift synthesized around the key); anything it + // doesn't cover falls back to the framework key character map. private void forwardText(@NonNull CharSequence text) { if (inputForwarder == null || !connected) return; - KeyEvent[] events = keyCharacterMap.getEvents(text.toString().toCharArray()); - if (events == null) return; // Wrap with the extra-keys panel modifiers so e.g. Ctrl+(typed key) reaches the guest. nativeExtraKeys.applyModifiers(true); - for (KeyEvent e : events) { - if (e.getAction() == KeyEvent.ACTION_DOWN) { - inputForwarder.sendKeyEvent(e.getKeyCode(), true); - } else if (e.getAction() == KeyEvent.ACTION_UP) { - inputForwarder.sendKeyEvent(e.getKeyCode(), false); + String s = text.toString(); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (inputForwarder.sendChar(c)) continue; + KeyEvent[] events = keyCharacterMap.getEvents(new char[]{c}); + if (events == null) continue; + for (KeyEvent e : events) { + if (e.getAction() == KeyEvent.ACTION_DOWN) { + inputForwarder.sendKeyEvent(e.getKeyCode(), true); + } else if (e.getAction() == KeyEvent.ACTION_UP) { + inputForwarder.sendKeyEvent(e.getKeyCode(), false); + } } } nativeExtraKeys.applyModifiers(false); @@ -318,15 +498,199 @@ private void tapKey(int keyCode) { nativeExtraKeys.applyModifiers(false); } + // TOUCH mode only: raw multi-touch stays on the surface view, which is sized to the guest + // aspect ratio, so offsets are zero and view coords normalize straight to the multi-touch + // device's fixed ABS range. (Touch coords stay in view-local space even when the three-finger + // zoom transform is applied - Android inverse-maps them.) MOUSE/TABLET decline here so the + // event bubbles up to the container. private boolean onSurfaceTouch(View v, MotionEvent event) { + if (inputMode != InputMode.TOUCH) return false; if (inputForwarder == null || v.getWidth() <= 0 || v.getHeight() <= 0) return false; - // The SurfaceView is sized to the guest aspect ratio, so offsets are zero and scale is - // simply guest/view per axis. - var tf = TouchScaleCalculator.compute(guestWidth, guestHeight, v.getWidth(), v.getHeight()); + if (isMouseButtonTouch(event)) return true; + var tf = TouchScaleCalculator.compute(v.getWidth(), v.getHeight()); inputForwarder.sendTouchEvent(event, tf.scaleX, tf.scaleY); return true; } + // MOUSE/TABLET gesture surface: the whole container is active; the translator pins gestures + // that carry guest coordinates to the rendered surface rect. + private boolean onContainerTouch(View v, MotionEvent event) { + if (inputMode == InputMode.TOUCH) return false; + if (inputForwarder == null || gestureTranslator == null) return false; + if (isMouseButtonTouch(event)) return true; + // TABLET absolute coords go to the normalized-range evdev absolute-mouse device; MOUSE + // REL deltas are guest px, so they scale against the guest resolution instead. + float unitW = inputMode == InputMode.TABLET + ? EvdevEncoder.NORMALIZED_ABS_MAX : guestWidth; + float unitH = inputMode == InputMode.TABLET + ? EvdevEncoder.NORMALIZED_ABS_MAX : guestHeight; + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + pointerDriveActive = true; + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + pointerDriveActive = false; + break; + default: + break; + } + return gestureTranslator.onTouchEvent(event, displayRectInContainer(), unitW, unitH); + } + + /** + * The guest moved its hardware cursor. Places the overlay and, only while the user is actually + * driving the pointer with a finger in relative mode, pans a zoomed view to keep it visible. + * + * The gate is deliberately narrow. In relative mode a short drag can send the guest pointer a + * long way, so it can leave a zoomed viewport without the finger ever nearing the screen edge + * -- that is the case worth following. A remote desktop session driving the same guest cursor + * must NOT drag this device's view around under someone else's hand. + */ + private void onGuestCursorMoved(int gx, int gy) { + // crosvm sends u32::MAX,MAX when the guest hides its pointer (UPDATE_CURSOR resource_id=0, + // which is what switching to a text console does). Without acting on it the overlay keeps + // showing the last cursor image on a console that should have none. A real position is a + // framebuffer coordinate, so this value can never be a genuine one. + if (gx == -1 && gy == -1) { // u32::MAX arrives as -1 in Java's signed int + lastCursorX = -1; + lastCursorY = -1; + parkCursorOverlay(); + return; + } + lastCursorX = gx; + lastCursorY = gy; + positionCursorOverlay(); + if (pointerDriveActive && inputMode == InputMode.MOUSE && viewport != null) { + viewport.panToShowContentPoint(gx, gy, CURSOR_FOLLOW_MARGIN_PX); + } + } + + /** + * Take the pointer off screen without letting go of its Surface. See {@link #CURSOR_PARKED_PX}. + */ + private void parkCursorOverlay() { + if (cursorView == null) { + return; + } + cursorView.setTranslationX(CURSOR_PARKED_PX); + cursorView.setTranslationY(CURSOR_PARKED_PX); + } + + /** + * Put the 64x64 cursor overlay where the guest says its pointer is. + * + * Same transform the scanout gets: content is centred in the container, scaled about its + * centre, then displaced by the pan offset. The overlay is scaled too -- the guest's cursor is + * in guest pixels, so at 2x zoom it has to double like everything else, or the pointer shrinks + * relative to what it is pointing at. Pivot goes to (0,0) so scaling grows the image away from + * the hotspot corner instead of around its middle. + */ + private void positionCursorOverlay() { + if (cursorView == null || vpBaseW <= 0 || guestWidth <= 0 || guestHeight <= 0) { + return; + } + if (lastCursorX < 0) { + return; // no position yet + } + View area = (View) surfaceView.getParent(); + if (area == null || area.getWidth() <= 0) { + return; + } + float vx = lastCursorX * vpBaseW / guestWidth; + float vy = lastCursorY * vpBaseH / guestHeight; + float cx = area.getWidth() / 2f + vpOffsetX + (vx - vpBaseW / 2f) * vpViewScale; + float cy = area.getHeight() / 2f + vpOffsetY + (vy - vpBaseH / 2f) * vpViewScale; + float pxPerGuestPx = (vpBaseW / (float) guestWidth) * vpViewScale; + + cursorView.setPivotX(0f); + cursorView.setPivotY(0f); + cursorView.setScaleX(pxPerGuestPx); + cursorView.setScaleY(pxPerGuestPx); + cursorView.setTranslationX(cx); + cursorView.setTranslationY(cy); + if (cursorView.getVisibility() != VISIBLE) { + cursorView.setVisibility(VISIBLE); + } + } + + // A hardware-mouse right/middle press also arrives on the touch stream (ACTION_DOWN with the + // button in buttonState). Those are delivered by the generic-motion handler; keep them out of + // the tap/gesture path (else right-click doubles as a left tap) but consume them so the + // framework doesn't synthesize a BACK key from an unhandled right-click. + private boolean isMouseButtonTouch(@NonNull MotionEvent event) { + if ((event.getSource() & android.view.InputDevice.SOURCE_MOUSE) != 0 + && (event.getButtonState() & (MotionEvent.BUTTON_SECONDARY + | MotionEvent.BUTTON_TERTIARY | MotionEvent.BUTTON_STYLUS_PRIMARY)) != 0) { + lastMouseButtonMs = android.os.SystemClock.uptimeMillis(); + return true; + } + return false; + } + + // Where the guest frame is rendered, in container coordinates: the letterbox-fitted surface + // bounds mapped through the viewport's current zoom/pan transform. + @NonNull + private RectF displayRectInContainer() { + var rect = new RectF(0, 0, surfaceView.getWidth(), surfaceView.getHeight()); + surfaceView.getMatrix().mapRect(rect); + rect.offset(surfaceView.getLeft(), surfaceView.getTop()); + return rect; + } + + // Host mouse/stylus scroll wheel and right/middle buttons (left stays on the touch/tap path). + // Button presses are ALWAYS consumed - an unhandled BUTTON_SECONDARY press is what makes the + // framework synthesize a BACK key, which must never fire inside the VM display. + private boolean onSurfaceGenericMotion(View v, MotionEvent event) { + switch (event.getActionMasked()) { + case MotionEvent.ACTION_SCROLL: + if (inputForwarder != null) { + inputForwarder.sendScroll( + Math.round(event.getAxisValue(MotionEvent.AXIS_VSCROLL)), + Math.round(event.getAxisValue(MotionEvent.AXIS_HSCROLL))); + } + return true; + case MotionEvent.ACTION_BUTTON_PRESS: + case MotionEvent.ACTION_BUTTON_RELEASE: { + lastMouseButtonMs = android.os.SystemClock.uptimeMillis(); + short btn = mapActionButton(event.getActionButton()); + if (btn != 0 && inputForwarder != null) { + inputForwarder.sendPointerButton(btn, + event.getActionMasked() == MotionEvent.ACTION_BUTTON_PRESS); + } + return true; + } + default: + return false; + } + } + + // Host pointer hover (no button): TABLET mode only - absolute hover on the guest tablet. + // MOUSE/TOUCH modes deliberately ignore Android-side hover. + private boolean onSurfaceHover(View v, MotionEvent event) { + if (inputForwarder == null || v.getWidth() <= 0 || v.getHeight() <= 0) return false; + if (inputMode != InputMode.TABLET) return false; + int action = event.getActionMasked(); + if (action == MotionEvent.ACTION_HOVER_MOVE || action == MotionEvent.ACTION_HOVER_ENTER) { + var tf = TouchScaleCalculator.compute(v.getWidth(), v.getHeight()); + inputForwarder.sendHover(event.getX(), event.getY(), tf.scaleX, tf.scaleY); + return true; + } + return false; + } + + private static short mapActionButton(int actionButton) { + switch (actionButton) { + case MotionEvent.BUTTON_SECONDARY: + case MotionEvent.BUTTON_STYLUS_PRIMARY: + return EvdevEncoder.BTN_RIGHT; + case MotionEvent.BUTTON_TERTIARY: + return EvdevEncoder.BTN_MIDDLE; + default: + return 0; + } + } + // Sink for InputForwarder: ships encoded evdev to the daemon, which owns the crosvm input // sockets and writes them to the guest. Called on the (single) InputForwarder worker thread, so // the synchronous request keeps events ordered and back-pressured. @@ -347,6 +711,15 @@ private boolean sendInputToDaemon(int channel, @NonNull byte[] data) { @Override public boolean dispatchKeyEvent(@NonNull KeyEvent event) { int keyCode = event.getKeyCode(); + // A hardware-mouse right-click the framework (or OEM ROM) failed to see consumed gets + // synthesized as a BACK key - sometimes mouse-sourced, sometimes (OEM injection) claiming + // a keyboard/virtual source. Swallow BACK when it's mouse-sourced OR arrives right after + // any mouse right/middle-button activity; the click itself already went to the guest. + if (keyCode == KeyEvent.KEYCODE_BACK + && ((event.getSource() & android.view.InputDevice.SOURCE_MOUSE) != 0 + || android.os.SystemClock.uptimeMillis() - lastMouseButtonMs + < MOUSE_BACK_SUPPRESS_MS)) + return true; if (keyCode == KeyEvent.KEYCODE_VOLUME_UP || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) return super.dispatchKeyEvent(event); if (inputForwarder != null && connected) { @@ -386,19 +759,83 @@ private static boolean isModifierKey(int keyCode) { } } - private void updateAspectRatio(int containerW, int containerH) { - if (containerW <= 0 || containerH <= 0 || guestWidth <= 0 || guestHeight <= 0) return; - float vmAspect = (float) guestWidth / guestHeight; - float containerAspect = (float) containerW / containerH; - int viewW, viewH; - if (vmAspect > containerAspect) { - viewW = containerW; - viewH = Math.round(containerW / vmAspect); - } else { - viewH = containerH; - viewW = Math.round(containerH * vmAspect); - } - surfaceView.setLayoutParams(new FrameLayout.LayoutParams(viewW, viewH, CENTER)); + // Wires the viewport controller (single writer of the SurfaceView geometry), the chrome + // controller (single writer of toolbar/status bar/extra keys/system bars visibility) and the + // window-insets listener that turns system bars + IME into root padding, which in turn sizes + // the display container. + private void setupLayoutControllers() { + int minAreaPx = Math.round(MIN_AREA_DP * getResources().getDisplayMetrics().density); + viewport = new DisplayViewportController(minAreaPx, + new DisplayViewportController.Listener() { + @Override + public void onViewportChanged(int baseW, int baseH, float viewScale, + float offsetX, float offsetY) { + surfaceView.setLayoutParams(new FrameLayout.LayoutParams(baseW, baseH, CENTER)); + surfaceView.setScaleX(viewScale); + surfaceView.setScaleY(viewScale); + surfaceView.setTranslationX(offsetX); + surfaceView.setTranslationY(offsetY); + vpBaseW = baseW; + vpBaseH = baseH; + vpViewScale = viewScale; + vpOffsetX = offsetX; + vpOffsetY = offsetY; + positionCursorOverlay(); + } + + @Override + public void onGuestResizeWanted(int areaW, int areaH) { + // Auto-resize Guest Display: no guest-side channel on this path yet. + } + }); + viewport.setContentSize(guestWidth, guestHeight); + + var inputPrefs = getSharedPreferences(INPUT_PREFS, MODE_PRIVATE); + chrome = new DisplayChromeController( + KeyboardMode.fromName(inputPrefs.getString(KEY_KEYBOARD_MODE, null)), + inputPrefs.getBoolean(KEY_ZONE_EXTRA, true), + inputPrefs.getBoolean(KEY_ZONE_FNX, false), + (fullscreen, mode, extraVisible, fnxVisible) -> { + toolbar.setVisibility(fullscreen ? GONE : VISIBLE); + statusBar.setVisibility(fullscreen ? GONE : VISIBLE); + extraKeysPanel.applyZones( + extraVisible, fnxVisible, mode == KeyboardMode.SYSTEM); + phyKeyboard.setZoneToggleState(extraVisible, fnxVisible); + phyKeyboard.setVisibleAnimated(mode == KeyboardMode.LAPTOP); + var controller = getWindow().getInsetsController(); + if (controller != null) { + if (fullscreen) { + controller.hide(WindowInsets.Type.systemBars()); + controller.setSystemBarsBehavior(BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); + } else { + controller.show(WindowInsets.Type.systemBars()); + } + } + // Re-request insets so the root padding (and thus the display area) updates in the + // same pass as the visibility changes. + ViewCompat.requestApplyInsets(findViewById(R.id.main)); + }); + chrome.setStateListener((mode, extraVisible, fnxVisible) -> inputPrefs.edit() + .putString(KEY_KEYBOARD_MODE, mode.name()) + .putBoolean(KEY_ZONE_EXTRA, extraVisible) + .putBoolean(KEY_ZONE_FNX, fnxVisible) + .apply()); + chrome.applyInitial(); + + View root = findViewById(R.id.main); + ViewCompat.setOnApplyWindowInsetsListener(root, (v, insets) -> { + Insets sysBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); + Insets ime = insets.getInsets(WindowInsetsCompat.Type.ime()); + // The Main row's shared slot follows the IME: Fn while it is up, "show IME" while + // it is down. The system owns that visibility, so read it rather than track it. + extraKeysPanel.setImeVisible(insets.isVisible(WindowInsetsCompat.Type.ime())); + boolean fullscreen = chrome != null && chrome.isFullscreen(); + int top = fullscreen ? 0 : sysBars.top; + int bottom = Math.max(fullscreen ? 0 : sysBars.bottom, ime.bottom); + v.setPadding(0, top, 0, bottom); + return insets; + }); + ViewCompat.requestApplyInsets(root); } private void setStatus(String text, int colorRes) { @@ -420,69 +857,181 @@ private void hideOverlay() { private void toggleSoftKeyboard() { var imm = getSystemService(InputMethodManager.class); - if (imm != null) tryShowKeyboard(imm, 10); + if (imm == null) return; + // Post so the fab-menu popup has finished tearing down: it still owns the touch-driven + // focus transition synchronously after the item click, so requesting focus + showing the + // IME inline lands before our editor is the served view and does nothing. + mainHandler.post(() -> tryShowKeyboard(imm, 15)); } // Drive a real (invisible) EditText: a SurfaceView is an unreliable IME target on some ROMs. - // The popup menu relinquishes window focus right before this runs, so the editor isn't yet - // "served" by the IMM and showSoftInput() is silently ignored (no ResultReceiver callback - // either). Retry on a short delay until the input connection is established; force as a last - // resort. + // showSoftInput() can return true for a view the IMM isn't serving yet and show nothing, so the + // success test is imm.isActive(editor), retried on a short delay until the input connection is + // live. The last few rounds force the IME (some ROMs ignore the implicit request). private void tryShowKeyboard(@NonNull InputMethodManager imm, int attemptsLeft) { + if (attemptsLeft <= 0 || isFinishing()) return; + keyboardInput.requestFocusFromTouch(); keyboardInput.requestFocus(); - if (imm.showSoftInput(keyboardInput, 0)) return; - if (attemptsLeft > 0) { - mainHandler.postDelayed(() -> tryShowKeyboard(imm, attemptsLeft - 1), 60); - } + int flag = attemptsLeft <= 3 + ? InputMethodManager.SHOW_FORCED : InputMethodManager.SHOW_IMPLICIT; + imm.showSoftInput(keyboardInput, flag); + if (keyboardInput.isFocused() && imm.isActive(keyboardInput)) return; + mainHandler.postDelayed(() -> tryShowKeyboard(imm, attemptsLeft - 1), 60); } private void toggleFullscreen() { - isFullscreen = !isFullscreen; - var controller = getWindow().getInsetsController(); - if (controller == null) return; - if (isFullscreen) { - toolbar.setVisibility(GONE); - statusBar.setVisibility(GONE); - extraKeysPanel.setVisibility(GONE); - controller.hide(WindowInsets.Type.systemBars()); - controller.setSystemBarsBehavior(BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); - } else { - toolbar.setVisibility(VISIBLE); - statusBar.setVisibility(VISIBLE); - extraKeysPanel.animateIn(); - controller.show(WindowInsets.Type.systemBars()); - } + chrome.toggleFullscreen(); } private void showFabMenu() { var popup = new MaterialMenu(this, fabMenu); popup.inflate(R.menu.menu_native_display_menu); + var header = new LinearLayout(this); + header.setOrientation(LinearLayout.VERTICAL); + header.addView(buildInputModeHeader(popup)); + header.addView(DisplayKeyboardMenuRow.build( + getLayoutInflater(), chrome.getKeyboardMode(), this::applyKeyboardMode, + popup::dismiss)); + popup.setHeaderView(header); popup.setOnMenuItemClickListener(this::onMenuItemClicked); popup.show(); } + // Menu header: one row of three icon buttons (touch / tablet / mouse), active mode checked. + private View buildInputModeHeader(MaterialMenu popup) { + var group = (com.google.android.material.button.MaterialButtonToggleGroup) + getLayoutInflater().inflate(R.layout.view_input_mode_toggle, null); + group.check(inputMode == InputMode.MOUSE ? R.id.mode_mouse + : inputMode == InputMode.TABLET ? R.id.mode_tablet : R.id.mode_touch); + group.addOnButtonCheckedListener((g, checkedId, isChecked) -> { + if (!isChecked) return; + setInputModeTo(checkedId == R.id.mode_mouse ? InputMode.MOUSE + : checkedId == R.id.mode_tablet ? InputMode.TABLET : InputMode.TOUCH); + popup.dismiss(); + }); + return group; + } + + // Selecting the system keyboard summons the IME; anything else puts it away, so the mode + // and what is actually on screen agree. + private void applyKeyboardMode(@NonNull KeyboardMode mode) { + chrome.setKeyboardMode(mode); + if (mode == KeyboardMode.SYSTEM) toggleSoftKeyboard(); + else hideSoftKeyboard(); + } + + // Dropping the editor's focus first matters: it is what the IME is attached to, and some + // ROMs re-show the keyboard for a still-focused editor right after a hide request. + private void hideSoftKeyboard() { + keyboardInput.clearFocus(); + var controller = WindowCompat.getInsetsController(getWindow(), keyboardInput); + controller.hide(WindowInsetsCompat.Type.ime()); + var imm = getSystemService(InputMethodManager.class); + if (imm != null) + imm.hideSoftInputFromWindow(findViewById(R.id.main).getWindowToken(), 0); + } + private boolean onMenuItemClicked(@NonNull MenuItem item) { int id = item.getItemId(); - if (id == R.id.menu_keyboard) { - toggleSoftKeyboard(); - return true; - } else if (id == R.id.menu_extra_keys) { - extraKeysPanel.setVisibleAnimated(extraKeysPanel.getVisibility() != VISIBLE); - return true; - } else if (id == R.id.menu_fullscreen) { + if (id == R.id.menu_fullscreen) { toggleFullscreen(); return true; + } else if (id == R.id.menu_rotate) { + toggleOrientation(); + return true; } return false; } + // Select TOUCH/MOUSE/TABLET: persist (shared with VNC) and route the InputForwarder + gesture + // translator to the matching virtio-input device. The segmented header shows the active mode. + private void setInputModeTo(@NonNull InputMode mode) { + if (inputMode == mode) return; + inputMode = mode; + getSharedPreferences(INPUT_PREFS, MODE_PRIVATE).edit() + .putInt(KEY_INPUT_MODE, inputMode.ordinal()).apply(); + if (inputForwarder != null) inputForwarder.setInputMode(inputMode); + if (gestureTranslator != null) { + gestureTranslator.setAbsolute(inputMode == InputMode.TABLET); + gestureTranslator.reset(); + } + } + + private void toggleOrientation() { + boolean landscape = getResources().getConfiguration().orientation + == android.content.res.Configuration.ORIENTATION_LANDSCAPE; + setRequestedOrientation(landscape + ? android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + : android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); + } + + /** + * Registry key for the VM-event callback, unique per instance. + * + * Not the tag: this activity is recreated (the orientation flip on entry does it), and the + * incoming instance's onStart() runs before the outgoing one's onStop() -- so a key shared by + * both has the one leaving unregister the one arriving, and the console then never hears that + * its VM exited. + */ + private final String eventKey = + fmt("%s@%s", TAG, Integer.toHexString(System.identityHashCode(this))); + + @Override + protected void onStart() { + super.onStart(); + var handler = ((DroidVMApp) getApplication()).getVMEventHandler(); + if (handler != null) handler.addForegroundCallback(eventKey, this); + } + + @Override + protected void onStop() { + super.onStop(); + var handler = ((DroidVMApp) getApplication()).getVMEventHandler(); + if (handler != null) handler.removeForegroundCallback(eventKey); + } + + /** + * The VM this console is attached to has gone. Close with it: what is left otherwise is a + * console that cannot reconnect, retrying a display service that will never be registered + * again (which used to cost the daemon dearly -- see NativeDisplayBinder). + * + * This is the exit event, not the absence of a crosvm process, and the difference is the + * point: a VM waiting for the huge-page reserve -- after a start, or between a guest reboot + * and its relaunch -- has no process either, and must not be mistaken for one that is gone. + * The daemon fires "rebooting" for that case and holds the exit event back until the VM + * really is not coming back, so there is nothing to second-guess here. + * + * A VM that crashed is the one case worth staying open for. VMEventHandler answers a non-zero + * exit by putting up the exit dialog -- the tail of the log, and a way into the full one -- + * on whatever activity is in front, and closing that activity out from under it would take + * the explanation with it. So the console holds; the user closes it after reading. The toast + * is that handler's job either way, so there is none here. + */ + @Override + public void onVMExited(UUID id, String vmName, int exitCode, JSONObject data) { + if (id == null || !id.toString().equals(vmId)) return; + if (exitCode != 0) { + Log.i(TAG, fmt("VM exited with %d -- keeping the console up for the exit dialog", + exitCode)); + return; + } + mainHandler.post(() -> { + if (isFinishing()) return; + Log.i(TAG, "VM stopped; closing the display"); + finish(); + }); + } + + // And keep the host's full-screen touch gestures (OEM three-finger screenshot etc.) + // from eating multi-finger input meant for the guest (see SystemGestureGuard). + SystemGestureGuard.enterDisplay(); + SystemGestureGuard.exitDisplay(); @Override protected void onDestroy() { super.onDestroy(); - extraKeysPanel.stopKeyRepeat(); - if (displayProvider != null) { - displayProvider.shutdown(); - displayProvider = null; + if (displaySource != null) { + displaySource.shutdown(); + displaySource = null; } if (inputForwarder != null) { inputForwarder.close(); @@ -492,20 +1041,10 @@ protected void onDestroy() { directSink.close(); directSink = null; } - if (binderReceiverRegistered) { - try { - unregisterReceiver(binderReceiver); - } catch (Exception ignored) { - } - binderReceiverRegistered = false; - } - if (rootService != null) { - try { - rootService.asBinder().unlinkToDeath(deathRecipient, 0); - } catch (Exception ignored) { - } + if (displayAttach != null) { + displayAttach.stop(); + displayAttach = null; } - rootService = null; } @NonNull diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/EvdevEncoder.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/EvdevEncoder.java index 9f3d6bc5c..f43cb89dc 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/EvdevEncoder.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/EvdevEncoder.java @@ -41,10 +41,32 @@ public final class EvdevEncoder { private static final short ABS_MT_POSITION_Y = 0x36; private static final short ABS_MT_TRACKING_ID = 0x39; + /** + * Fixed ABS_X/ABS_Y (and ABS_MT_POSITION_X/Y) maximum the guest absolute-mouse / multi-touch + * devices advertise when the daemon omits an explicit resolution ({@code --input + * absolute-mouse}/{@code multi-touch} with no width/height). View coordinates are scaled to this + * range against the on-screen view size, so the guest maps them 1:1 to its screen at any + * resolution -- resolution-independent and auto-resize-proof. MUST equal crosvm's + * {@code NORMALIZED_ABS_MAX} (config.rs) and {@code VNC_ABS_MAX} (gpu_display_vnc.rs). + */ + public static final int NORMALIZED_ABS_MAX = 0x7FFF; + private static final short BTN_TOUCH = 0x14a; + // Relative mouse (InputMode.MOUSE). + private static final short EV_REL = 0x02; + private static final short REL_X = 0x00; + private static final short REL_Y = 0x01; + private static final short REL_WHEEL = 0x08; + private static final short REL_HWHEEL = 0x06; + public static final short BTN_LEFT = 0x110; + public static final short BTN_RIGHT = 0x111; + public static final short BTN_MIDDLE = 0x112; + /** Live Android pointer id -> evdev MT slot. Touched only on the worker thread. */ private final Map pointerSlots = new HashMap<>(); + /** Last guest-space position sent per live pointer id, for the keepalive re-send. */ + private final Map pointerPos = new HashMap<>(); public EvdevEncoder() { } @@ -86,6 +108,93 @@ public static byte[] encodeKey(short scanCode, boolean down) { return encode(events); } + /** Relative pointer motion for {@code InputMode.MOUSE}; null if there is no movement. */ + @Nullable + public static byte[] encodeMouseMove(int dx, int dy) { + if (dx == 0 && dy == 0) return null; + var events = new ArrayList(3); + if (dx != 0) events.add(new Event(EV_REL, REL_X, dx)); + if (dy != 0) events.add(new Event(EV_REL, REL_Y, dy)); + events.add(new Event(EV_SYN, SYN_REPORT, 0)); + return encode(events); + } + + /** Mouse button ({@link #BTN_LEFT}/{@link #BTN_RIGHT}/{@link #BTN_MIDDLE}) press or release. */ + @NonNull + public static byte[] encodeMouseButton(short button, boolean down) { + var events = new ArrayList(2); + events.add(new Event(EV_KEY, button, down ? 1 : 0)); + events.add(new Event(EV_SYN, SYN_REPORT, 0)); + return encode(events); + } + + /** Scroll wheel: vertical (REL_WHEEL, +up/-down) and horizontal (REL_HWHEEL) notches; null if 0. */ + @Nullable + public static byte[] encodeMouseWheel(int vNotches, int hNotches) { + if (vNotches == 0 && hNotches == 0) return null; + var events = new ArrayList(3); + if (vNotches != 0) events.add(new Event(EV_REL, REL_WHEEL, vNotches)); + if (hNotches != 0) events.add(new Event(EV_REL, REL_HWHEEL, hNotches)); + events.add(new Event(EV_SYN, SYN_REPORT, 0)); + return encode(events); + } + + /** + * Absolute-mouse "tablet" ({@code InputMode.TABLET}): the primary pointer mapped onto the guest + * absolute mouse's ABS_X/ABS_Y, with BTN_LEFT for the touch/click. Because the guest device is an + * absolute pointer (qemu usb-tablet), not a BTN_TOUCH touchscreen, the same device also carries + * hover ({@link #encodeAbsMove}), right/middle click ({@link #encodeMouseButton}) and scroll + * ({@link #encodeMouseWheel}). ABS range must equal the guest resolution + * ({@code --input absolute-mouse[width=guestW,height=guestH]}). + * + * @param scaleX guestWidth / viewWidth + * @param scaleY guestHeight / viewHeight + */ + @Nullable + public byte[] encodeTablet(@NonNull MotionEvent event, float scaleX, float scaleY) { + int x = (int) (event.getX() * scaleX); + int y = (int) (event.getY() * scaleY); + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: { + var events = new ArrayList(4); + events.add(new Event(EV_ABS, ABS_X, x)); + events.add(new Event(EV_ABS, ABS_Y, y)); + events.add(new Event(EV_KEY, BTN_LEFT, 1)); + events.add(new Event(EV_SYN, SYN_REPORT, 0)); + return encode(events); + } + case MotionEvent.ACTION_MOVE: { + var events = new ArrayList(3); + events.add(new Event(EV_ABS, ABS_X, x)); + events.add(new Event(EV_ABS, ABS_Y, y)); + events.add(new Event(EV_SYN, SYN_REPORT, 0)); + return encode(events); + } + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: { + var events = new ArrayList(2); + events.add(new Event(EV_KEY, BTN_LEFT, 0)); + events.add(new Event(EV_SYN, SYN_REPORT, 0)); + return encode(events); + } + default: + return null; + } + } + + /** + * Absolute pointer position with no button held (hover) for the absolute-mouse/tablet device. + * Coordinates are already in guest space. + */ + @NonNull + public static byte[] encodeAbsMove(int x, int y) { + var events = new ArrayList(3); + events.add(new Event(EV_ABS, ABS_X, x)); + events.add(new Event(EV_ABS, ABS_Y, y)); + events.add(new Event(EV_SYN, SYN_REPORT, 0)); + return encode(events); + } + /** * Encodes a touch {@link MotionEvent} into multi-touch evdev records, or null if there is * nothing to send. The multi-touch device's ABS range must equal the guest resolution passed @@ -106,6 +215,7 @@ public byte[] encodeTouch(@NonNull MotionEvent event, float scaleX, float scaleY if (slot == null) continue; // no DOWN seen for this pointer; ignore int x = (int) (event.getX(i) * scaleX); int y = (int) (event.getY(i) * scaleY); + pointerPos.put(id, new int[]{x, y}); events.add(new Event(EV_ABS, ABS_MT_SLOT, slot)); events.add(new Event(EV_ABS, ABS_MT_POSITION_X, x)); events.add(new Event(EV_ABS, ABS_MT_POSITION_Y, y)); @@ -125,6 +235,7 @@ public byte[] encodeTouch(@NonNull MotionEvent event, float scaleX, float scaleY int slot = allocSlot(id); int x = (int) (event.getX(idx) * scaleX); int y = (int) (event.getY(idx) * scaleY); + pointerPos.put(id, new int[]{x, y}); var events = new ArrayList(8); // Only the first contact toggles BTN_TOUCH; further fingers must not re-assert it. if (pointerSlots.size() == 1) @@ -145,6 +256,7 @@ public byte[] encodeTouch(@NonNull MotionEvent event, float scaleX, float scaleY int id = event.getPointerId(event.getActionIndex()); Integer slot = pointerSlots.remove(id); if (slot == null) return null; + pointerPos.remove(id); var events = new ArrayList(4); events.add(new Event(EV_ABS, ABS_MT_SLOT, slot)); events.add(new Event(EV_ABS, ABS_MT_TRACKING_ID, -1)); @@ -163,6 +275,7 @@ public byte[] encodeTouch(@NonNull MotionEvent event, float scaleX, float scaleY events.add(new Event(EV_ABS, ABS_MT_TRACKING_ID, -1)); } pointerSlots.clear(); + pointerPos.clear(); events.add(new Event(EV_KEY, BTN_TOUCH, 0)); events.add(new Event(EV_SYN, SYN_REPORT, 0)); return encode(events); @@ -172,6 +285,39 @@ public byte[] encodeTouch(@NonNull MotionEvent event, float scaleX, float scaleY } } + /** Whether any touch contact is currently down (keepalive needed while true). */ + public boolean hasTouchContacts() { + return !pointerSlots.isEmpty(); + } + + /** + * Re-sends every live contact at its last position, or null if there are no contacts. Real + * touch hardware reports at scan rate for as long as a finger is on the glass, even when + * nothing changes, and guests rely on that: the Windows touch stack ages out a contact whose + * reports stop (a stationary finger otherwise reads as a lift, then its next micro-movement + * as a fresh touchdown). This frame is what a quiet scan cycle would have produced. + */ + @Nullable + public byte[] encodeTouchKeepalive() { + if (pointerSlots.isEmpty()) return null; + var events = new ArrayList(pointerSlots.size() * 3 + 3); + for (var entry : pointerSlots.entrySet()) { + int[] pos = pointerPos.get(entry.getKey()); + if (pos == null) continue; + int slot = entry.getValue(); + events.add(new Event(EV_ABS, ABS_MT_SLOT, slot)); + events.add(new Event(EV_ABS, ABS_MT_POSITION_X, pos[0])); + events.add(new Event(EV_ABS, ABS_MT_POSITION_Y, pos[1])); + if (slot == 0) { + events.add(new Event(EV_ABS, ABS_X, pos[0])); + events.add(new Event(EV_ABS, ABS_Y, pos[1])); + } + } + if (events.isEmpty()) return null; + events.add(new Event(EV_SYN, SYN_REPORT, 0)); + return encode(events); + } + /** Maps a pointer id to a stable slot, allocating the lowest free slot index if new. */ private int allocSlot(int pointerId) { Integer existing = pointerSlots.get(pointerId); diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/InputForwarder.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/InputForwarder.java index ef001df42..9b4c8f5db 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/InputForwarder.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/InputForwarder.java @@ -4,17 +4,21 @@ package cn.classfun.droidvm.ui.vm.display.nativedisplay.input; import static cn.classfun.droidvm.lib.store.vm.NativeDisplay.KEYBOARD; +import static cn.classfun.droidvm.lib.store.vm.NativeDisplay.MOUSE; import static cn.classfun.droidvm.lib.store.vm.NativeDisplay.MULTITOUCH; +import static cn.classfun.droidvm.lib.store.vm.NativeDisplay.TABLET; import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; -import android.os.SystemClock; +import cn.classfun.droidvm.ui.vm.display.base.InputMode; + import android.util.Log; import android.view.MotionEvent; import androidx.annotation.NonNull; -import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; /** @@ -24,10 +28,14 @@ * serialized on one background thread; MotionEvents are copied first because the framework recycles * the originals. * - * ACTION_MOVE is coalesced: the synchronous per-event IPC round-trip is far slower than the touch - * sample rate, so unbounded moves would pile up in the worker queue and the on-screen pointer would - * fall seconds behind the finger. Only the newest pending move is kept; discrete DOWN/UP/POINTER_* - * events are never dropped and stay ordered. MVP scope: touch + keyboard. + * Pointer motion is coalesced: the synchronous per-event IPC round-trip is far slower than the + * touch sample rate, so unbounded moves would pile up in the worker queue and the on-screen pointer + * would fall seconds behind the finger. Each motion stream keeps one pending slot with at most one + * drain task in the queue: touch ACTION_MOVE keeps the newest frame, relative mouse deltas sum up + * (a sum of deltas is one correct larger delta), tablet/hover positions keep the newest position. + * Discrete events (buttons, wheel, DOWN/UP) are never dropped. FIFO keeps a discrete event behind + * all motion submitted before it; motion submitted after it may fold into an older pending slot + * and arrive up to one frame early, the same tradeoff the touch path already makes. */ public final class InputForwarder { private static final String TAG = "InputForwarder"; @@ -40,15 +48,43 @@ public interface InputSink { private final InputSink sink; // Stateful touch encoder (pointer-id -> slot, contact count); single-threaded on the worker. private final EvdevEncoder encoder = new EvdevEncoder(); - private final ExecutorService worker = Executors.newSingleThreadExecutor(r -> { + + // Current pointer mode; set from the UI thread, read on the worker. Volatile suffices: a change + // just takes effect on the next event. TOUCH -> multi-touch, MOUSE -> relative, TABLET -> single. + private volatile InputMode inputMode = InputMode.TOUCH; + // Mouse-mode gesture state (InputMode.MOUSE); touched only on the worker thread. + private float mouseLastX, mouseLastY, mouseDownX, mouseDownY; + private long mouseDownTime; + private boolean mouseDragging; + private static final float MOUSE_TAP_SLOP = 16f; // guest px of travel before a press is a drag + private static final long MOUSE_TAP_MS = 250; // max press duration still treated as a tap + // Touch keepalive period. Real touch hardware reports at scan rate while a finger is on the + // glass; the Windows touch stack ages out a contact whose reports stop, so a stationary finger + // (Android sends MOVE only on change) reads as lift + fresh touchdown on its next twitch. Well + // under any plausible aging threshold (real digitizers scan at 60Hz+), still only ~20 frames/s. + private static final long TOUCH_KEEPALIVE_MS = 50; + private final ScheduledExecutorService worker = Executors.newSingleThreadScheduledExecutor(r -> { var t = new Thread(r, "InputForwarder"); t.setDaemon(true); return t; }); + // Whether a keepalive tick is scheduled; touched only on the worker thread. + private boolean keepaliveScheduled; // Newest pending ACTION_MOVE, coalesced so a fast finger can't outrun the synchronous IPC. private final AtomicReference pendingMove = new AtomicReference<>(); + // Pending-motion slots for the MOUSE/TABLET gesture paths, guarded by motionLock. Each slot + // has at most one drain task in the worker queue (the *Pending flag); producers on the UI + // thread fold into the slot while a drain is queued instead of submitting more tasks. + private final Object motionLock = new Object(); + private int pendMouseDx, pendMouseDy; // relative mouse motion, summed (guest px) + private boolean mouseMovePending; + private int pendAbsX, pendAbsY; // tablet absolute position, newest wins (guest px) + private boolean absMovePending; + private int pendHoverX, pendHoverY; // hover position, newest wins (guest px) + private boolean hoverPending; + private static final class TouchFrame { final MotionEvent event; final float scaleX; @@ -65,6 +101,142 @@ public InputForwarder(@NonNull InputSink sink) { this.sink = sink; } + /** Switches pointer mode at runtime; takes effect on the next touch event. */ + public void setInputMode(@NonNull InputMode mode) { + this.inputMode = mode; + } + + /** The guest pointer device the current mode routes host mouse/stylus events to. */ + private int pointerChannel() { + return inputMode == InputMode.TABLET ? TABLET : MOUSE; + } + + /** + * A pointer button ({@link EvdevEncoder#BTN_RIGHT}/{@link EvdevEncoder#BTN_MIDDLE}) press/release + * from a host mouse or stylus. Left-click still rides the touch/tap path. Routed to the tablet + * (absolute mouse) in TABLET mode, otherwise the relative mouse. + */ + public void sendPointerButton(short button, boolean down) { + submit("pointerButton", () -> sink.write(pointerChannel(), + EvdevEncoder.encodeMouseButton(button, down))); + } + + /** + * Relative cursor motion (guest px) on the relative-mouse device; the guest renders the cursor. + * Deltas are summed into the pending slot, so any number of calls between two worker turns + * still costs one IPC round-trip and the pointer can't fall behind the finger. + */ + public void sendMouseMove(int dxGuest, int dyGuest) { + if (dxGuest == 0 && dyGuest == 0) return; + boolean schedule; + synchronized (motionLock) { + pendMouseDx += dxGuest; + pendMouseDy += dyGuest; + schedule = !mouseMovePending; + mouseMovePending = true; + } + if (schedule) submit("mouseMove", this::drainMouseMove); + } + + /** Absolute pointer position (guest px, no button change) on the tablet device; newest wins. */ + public void sendAbsMove(int xGuest, int yGuest) { + boolean schedule; + synchronized (motionLock) { + pendAbsX = xGuest; + pendAbsY = yGuest; + schedule = !absMovePending; + absMovePending = true; + } + if (schedule) submit("absMove", this::drainAbsMove); + } + + /** Left button on the tablet device, positioned first so the press lands at (x, y) guest px. */ + public void sendAbsLeftButton(boolean down, int xGuest, int yGuest) { + submit("absLeft", () -> { + sink.write(TABLET, EvdevEncoder.encodeAbsMove(xGuest, yGuest)); + sink.write(TABLET, EvdevEncoder.encodeMouseButton(EvdevEncoder.BTN_LEFT, down)); + }); + } + + /** Host scroll wheel (vertical, horizontal notches) routed to the active pointer device. */ + public void sendScroll(int vNotches, int hNotches) { + submit("scroll", () -> { + byte[] data = EvdevEncoder.encodeMouseWheel(vNotches, hNotches); + if (data != null) sink.write(pointerChannel(), data); + }); + } + + /** + * Host pointer hover (no button held). In TABLET mode it becomes an absolute position on the + * guest tablet (native hover); otherwise a relative delta so the guest mouse cursor follows. + * Coordinates are view pixels; scale maps them to guest space. + */ + public void sendHover(float viewX, float viewY, float scaleX, float scaleY) { + int gx = (int) (viewX * scaleX); + int gy = (int) (viewY * scaleY); + boolean schedule; + synchronized (motionLock) { + pendHoverX = gx; + pendHoverY = gy; + schedule = !hoverPending; + hoverPending = true; + } + if (schedule) submit("hover", this::drainHover); + } + + // The drains below run on the worker thread only. Between a drain's read-and-clear and its + // sink.write, a producer may refill the slot and queue the next drain task; nothing is lost + // and the queue still holds at most one task per stream. + + private void drainMouseMove() { + int dx, dy; + synchronized (motionLock) { + dx = pendMouseDx; + dy = pendMouseDy; + pendMouseDx = 0; + pendMouseDy = 0; + mouseMovePending = false; + } + // dx/dy may legitimately sum to zero (back-and-forth motion); encode returns null then. + byte[] data = EvdevEncoder.encodeMouseMove(dx, dy); + if (data != null) sink.write(MOUSE, data); + } + + private void drainAbsMove() { + int x, y; + boolean had; + synchronized (motionLock) { + had = absMovePending; + x = pendAbsX; + y = pendAbsY; + absMovePending = false; + } + if (!had) return; + sink.write(TABLET, EvdevEncoder.encodeAbsMove(x, y)); + } + + private void drainHover() { + int gx, gy; + boolean had; + synchronized (motionLock) { + had = hoverPending; + gx = pendHoverX; + gy = pendHoverY; + hoverPending = false; + } + if (!had) return; + if (inputMode == InputMode.TABLET) { + sink.write(TABLET, EvdevEncoder.encodeAbsMove(gx, gy)); + } else { + int dx = Math.round(gx - mouseLastX); + int dy = Math.round(gy - mouseLastY); + mouseLastX = gx; + mouseLastY = gy; + byte[] data = EvdevEncoder.encodeMouseMove(dx, dy); + if (data != null) sink.write(MOUSE, data); + } + } + private void submit(@NonNull String name, @NonNull Runnable block) { try { worker.execute(() -> { @@ -107,29 +279,100 @@ private void drainMove() { } private void sendTouchNow(@NonNull MotionEvent event, float scaleX, float scaleY) { - // eventTime is on the SystemClock.uptimeMillis() timebase, so the diff below is the full - // finger-to-sink latency (kernel input -> framework dispatch -> our worker -> sink). Read it - // before recycle() since the framework reuses the MotionEvent afterwards. - long eventTimeMs = event.getEventTime(); - byte[] data; try { - data = encoder.encodeTouch(event, scaleX, scaleY); + switch (inputMode) { + case MOUSE: + sendMouseNow(event, scaleX, scaleY); + break; + case TABLET: { + byte[] data = encoder.encodeTablet(event, scaleX, scaleY); + if (data != null) sink.write(TABLET, data); + break; + } + case TOUCH: + default: { + byte[] data = encoder.encodeTouch(event, scaleX, scaleY); + if (data != null) sink.write(MULTITOUCH, data); + scheduleTouchKeepalive(); + break; + } + } } catch (Exception e) { - Log.e(TAG, "encode touch failed", e); - return; + Log.e(TAG, "encode/send pointer failed", e); } finally { event.recycle(); } - if (data == null) return; // nothing to send for this event - long sinkStartNs = System.nanoTime(); - boolean ok = sink.write(MULTITOUCH, data); - double sinkMs = (System.nanoTime() - sinkStartNs) / 1_000_000.0; - long e2eMs = SystemClock.uptimeMillis() - eventTimeMs; - // direct=false means the write fell back to the vm_input JSON-RPC IPC path (~40ms) instead - // of the direct unix socket (<1ms) - i.e. the UI couldn't reach the daemon's UI input socket. - boolean direct = sink instanceof DirectInputSink && ((DirectInputSink) sink).wasLastWriteDirect(); - Log.d(TAG, fmt("touch latency: sink=%.2fms e2e=%dms direct=%b delivered=%b", - sinkMs, e2eMs, direct, ok)); + } + + // Worker thread only. One tick in flight at a time; the chain ends itself when the last + // contact lifts (encodeTouchKeepalive returns null) or the mode leaves TOUCH, and any touch + // event while contacts are down restarts it. + private void scheduleTouchKeepalive() { + if (keepaliveScheduled || !encoder.hasTouchContacts()) return; + keepaliveScheduled = true; + try { + worker.schedule(this::touchKeepaliveTick, TOUCH_KEEPALIVE_MS, TimeUnit.MILLISECONDS); + } catch (Exception e) { + keepaliveScheduled = false; + } + } + + private void touchKeepaliveTick() { + keepaliveScheduled = false; + try { + if (inputMode != InputMode.TOUCH) return; + byte[] data = encoder.encodeTouchKeepalive(); + if (data == null) return; + sink.write(MULTITOUCH, data); + } catch (Exception e) { + Log.e(TAG, "touch keepalive failed", e); + } + scheduleTouchKeepalive(); + } + + // Relative-mouse translation (InputMode.MOUSE): a drag becomes REL_X/REL_Y motion, a quick tap + // without travel becomes a left click. Runs on the worker thread, so the mouse state needs no + // locking. Move coalescing upstream is fine here: the delta is measured from the last position + // we actually sent, so dropped intermediate samples just fold into one larger (correct) delta. + private void sendMouseNow(@NonNull MotionEvent event, float scaleX, float scaleY) { + float gx = event.getX() * scaleX; + float gy = event.getY() * scaleY; + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + mouseLastX = gx; + mouseLastY = gy; + mouseDownX = gx; + mouseDownY = gy; + mouseDownTime = event.getEventTime(); + mouseDragging = false; + break; + case MotionEvent.ACTION_MOVE: { + int dx = Math.round(gx - mouseLastX); + int dy = Math.round(gy - mouseLastY); + if (dx == 0 && dy == 0) break; + mouseLastX = gx; + mouseLastY = gy; + if (!mouseDragging + && (Math.abs(gx - mouseDownX) > MOUSE_TAP_SLOP + || Math.abs(gy - mouseDownY) > MOUSE_TAP_SLOP)) { + mouseDragging = true; + } + byte[] data = EvdevEncoder.encodeMouseMove(dx, dy); + if (data != null) sink.write(MOUSE, data); + break; + } + case MotionEvent.ACTION_UP: { + boolean tap = !mouseDragging + && (event.getEventTime() - mouseDownTime) <= MOUSE_TAP_MS; + if (tap) { + sink.write(MOUSE, EvdevEncoder.encodeMouseButton(EvdevEncoder.BTN_LEFT, true)); + sink.write(MOUSE, EvdevEncoder.encodeMouseButton(EvdevEncoder.BTN_LEFT, false)); + } + break; + } + default: + break; + } } /** @@ -157,6 +400,25 @@ public boolean sendKeyEvent(int keyCode, boolean pressed) { return true; } + /** + * Sends a printable character as a US-layout evdev key tap, wrapping it in LEFTSHIFT down/up + * when the character requires Shift (uppercase letters, {@code !@#...}). This is the path for + * soft keyboards that commit text rather than emitting key events, so uppercase and symbols + * reach the guest correctly instead of being lost or arriving lowercase. + * + * @return true if the character is mapped to a US-layout key; false if the caller should fall + * back to the framework key character map. + */ + public boolean sendChar(char c) { + KeyCodeMapper.CharKey key = KeyCodeMapper.charToKey(c); + if (key == null) return false; + if (key.shift) sendRawKeyEvent(KeyCodeMapper.KEY_LEFTSHIFT, true); + sendRawKeyEvent(key.scanCode, true); + sendRawKeyEvent(key.scanCode, false); + if (key.shift) sendRawKeyEvent(KeyCodeMapper.KEY_LEFTSHIFT, false); + return true; + } + /** Sends a raw Linux evdev KEY_* scan code. */ public void sendRawKeyEvent(int scanCode, boolean pressed) { submit("sendRawKeyEvent", () -> { diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/KeyCodeMapper.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/KeyCodeMapper.java index 22b3a0e24..f3a9114c9 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/KeyCodeMapper.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/KeyCodeMapper.java @@ -152,4 +152,70 @@ public static int shiftSynthBase(int keyCode) { return -1; } } + + /** + * A printable character resolved to a US-QWERTY evdev key plus whether Shift must be held. + * The guest keymap is assumed US; the caller wraps {@link #scanCode} with LEFTSHIFT when + * {@link #shift} is set (see InputForwarder.sendChar), which is the "insert shift up/down" + * behaviour needed for uppercase letters and shifted symbols the soft keyboard commits as text. + */ + public static final class CharKey { + public final short scanCode; + public final boolean shift; + + CharKey(int scanCode, boolean shift) { + this.scanCode = (short) scanCode; + this.shift = shift; + } + } + + private static final Map CHAR_MAP = new HashMap<>(); + + private static void ch(char c, int scanCode, boolean shift) { + CHAR_MAP.put(c, new CharKey(scanCode, shift)); + } + + static { + // Letters: lowercase unshifted, uppercase shifted. evdev scan codes follow the physical + // QWERTY layout, NOT the alphabet, so map each letter to its own KEY_* (KEY_A + (c-'a') + // would send e.g. 'q' as KEY_C). + int[] letterKeys = { + KEY_A, KEY_B, KEY_C, KEY_D, KEY_E, KEY_F, KEY_G, KEY_H, KEY_I, KEY_J, KEY_K, KEY_L, + KEY_M, KEY_N, KEY_O, KEY_P, KEY_Q, KEY_R, KEY_S, KEY_T, KEY_U, KEY_V, KEY_W, KEY_X, + KEY_Y, KEY_Z, + }; + for (int i = 0; i < 26; i++) { + ch((char) ('a' + i), letterKeys[i], false); + ch((char) ('A' + i), letterKeys[i], true); + } + // Digit row, unshifted. + int[] digitKeys = {KEY_0, KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6, KEY_7, KEY_8, KEY_9}; + for (int d = 0; d <= 9; d++) ch((char) ('0' + d), digitKeys[d], false); + // Digit row, shifted symbols (US layout). + ch(')', KEY_0, true); ch('!', KEY_1, true); ch('@', KEY_2, true); ch('#', KEY_3, true); + ch('$', KEY_4, true); ch('%', KEY_5, true); ch('^', KEY_6, true); ch('&', KEY_7, true); + ch('*', KEY_8, true); ch('(', KEY_9, true); + // Punctuation pairs (unshifted / shifted). + ch('-', KEY_MINUS, false); ch('_', KEY_MINUS, true); + ch('=', KEY_EQUAL, false); ch('+', KEY_EQUAL, true); + ch('[', KEY_LEFTBRACE, false); ch('{', KEY_LEFTBRACE, true); + ch(']', KEY_RIGHTBRACE, false); ch('}', KEY_RIGHTBRACE, true); + ch('\\', KEY_BACKSLASH, false); ch('|', KEY_BACKSLASH, true); + ch(';', KEY_SEMICOLON, false); ch(':', KEY_SEMICOLON, true); + ch('\'', KEY_APOSTROPHE, false); ch('"', KEY_APOSTROPHE, true); + ch('`', KEY_GRAVE, false); ch('~', KEY_GRAVE, true); + ch(',', KEY_COMMA, false); ch('<', KEY_COMMA, true); + ch('.', KEY_DOT, false); ch('>', KEY_DOT, true); + ch('/', KEY_SLASH, false); ch('?', KEY_SLASH, true); + // Whitespace. + ch(' ', KEY_SPACE, false); ch('\t', KEY_TAB, false); ch('\n', KEY_ENTER, false); + } + + /** + * Resolves a printable character to a US-QWERTY evdev key (+ Shift flag), or null if the + * character has no direct US-layout key (caller should fall back to the framework keymap). + */ + public static CharKey charToKey(char c) { + return CHAR_MAP.get(c); + } } diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/NativeExtraKeysPanel.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/NativeExtraKeysPanel.java index f85f30eff..aeed31bf1 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/NativeExtraKeysPanel.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/NativeExtraKeysPanel.java @@ -3,10 +3,6 @@ // Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.vm.display.nativedisplay.input; -import static android.view.KeyEvent.KEYCODE_CAPS_LOCK; -import static android.view.KeyEvent.KEYCODE_MINUS; -import static android.view.KeyEvent.KEYCODE_SLASH; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -15,8 +11,8 @@ /** * Adapts the shared {@link DisplayExtraKeysPanel} to the native backend, emitting evdev key events - * through {@link InputForwarder}. The sticky-modifier handling lives in {@link BaseExtraKeysAdapter}; - * only the emit/ready hooks and the repeat/caps key mapping are backend-specific. + * through {@link InputForwarder}. The sticky-modifier and key down/up handling live in + * {@link BaseExtraKeysAdapter}; only the emit/ready hooks are backend-specific. */ public final class NativeExtraKeysPanel extends BaseExtraKeysAdapter { @Nullable @@ -39,22 +35,4 @@ protected void emitKey(int androidKeyCode, boolean down) { protected boolean isReady() { return forwarder != null; } - - @Override - public void onKeyRepeat(int androidKeyCode) { - tapKey(androidKeyCode); - } - - @Override - public void onCharRepeat(char ch) { - if (ch == '/') tapKey(KEYCODE_SLASH); - else if (ch == '-') tapKey(KEYCODE_MINUS); - } - - @Override - public void onCapsToggle(boolean active) { - if (!isReady()) return; - emitKey(KEYCODE_CAPS_LOCK, true); - emitKey(KEYCODE_CAPS_LOCK, false); - } } diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/TouchScaleCalculator.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/TouchScaleCalculator.java index e677bf071..23ca2aee0 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/TouchScaleCalculator.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/TouchScaleCalculator.java @@ -4,10 +4,11 @@ package cn.classfun.droidvm.ui.vm.display.nativedisplay.input; /** - * Computes per-axis touch scale from view size to guest resolution. The touch listener is attached - * to the SurfaceView, which is itself sized to the guest aspect ratio (see - * {@code VMNativeDisplayActivity#updateAspectRatio}), so its bounds carry no letterbox/pillarbox - * bars: there is no offset and scale is simply guest/view per axis. + * Computes per-axis touch scale from view size to the guest device's fixed normalized ABS range + * guest auto-resize. The touch listener is attached to the SurfaceView, which is itself sized to + * the guest aspect ratio (see {@code VMNativeDisplayActivity#updateAspectRatio}), so its bounds + * carry no letterbox/pillarbox bars: there is no offset and scale is simply NORMALIZED_ABS_MAX/view + * per axis. */ public final class TouchScaleCalculator { private TouchScaleCalculator() { @@ -23,12 +24,15 @@ public static final class TouchTransform { } } - public static TouchTransform compute(int guestWidth, int guestHeight, - int viewWidth, int viewHeight) { - if (viewWidth <= 0 || viewHeight <= 0 || guestWidth <= 0 || guestHeight <= 0) { + public static TouchTransform compute(int viewWidth, int viewHeight) { + if (viewWidth <= 0 || viewHeight <= 0) { return new TouchTransform(1f, 1f); } - return new TouchTransform((float) guestWidth / viewWidth, - (float) guestHeight / viewHeight); + // Normalize view coords to the fixed ABS range (EvdevEncoder.NORMALIZED_ABS_MAX): the guest + // then maps them 1:1 to its screen at ANY resolution, so no guest resolution is needed here. + // Auto-resize just changes the view size, which this already tracks per call. + return new TouchTransform( + (float) EvdevEncoder.NORMALIZED_ABS_MAX / viewWidth, + (float) EvdevEncoder.NORMALIZED_ABS_MAX / viewHeight); } } diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/base/BaseVncActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/base/BaseVncActivity.java index 4b44ca8ee..5004071d0 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/base/BaseVncActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/base/BaseVncActivity.java @@ -147,8 +147,10 @@ protected void onCreate(Bundle savedInstanceState) { if (vmId == null) vmId = ""; bindViews(); setupToolbar(); - onSetupActivity(); + // Before onSetupActivity() so subclasses can wire views (e.g. the physical keyboard) + // to the adapter during their setup. vncExtraKeys = new VncExtraKeysPanel(extraKeysPanel); + onSetupActivity(); fetchVncInfoAndConnect(); } @@ -156,7 +158,6 @@ protected void onCreate(Bundle savedInstanceState) { protected void onDestroy() { super.onDestroy(); onDestroyExtra(); - extraKeysPanel.stopKeyRepeat(); running = false; if (vncClient != null) vncClient.requestStop(); executor.shutdown(); @@ -421,6 +422,12 @@ protected void hideConnectingOverlay() { @Override public boolean dispatchKeyEvent(@NonNull KeyEvent event) { int keyCode = event.getKeyCode(); + // A hardware-mouse right-click the framework (or OEM ROM) failed to see consumed gets + // synthesized as a mouse-sourced BACK key. Inside the VM display that must never navigate + // back - the right-click itself is delivered to the guest by the pointer handlers. + if (keyCode == android.view.KeyEvent.KEYCODE_BACK + && (event.getSource() & android.view.InputDevice.SOURCE_MOUSE) != 0) + return true; if (keyCode == KEYCODE_VOLUME_UP || keyCode == KEYCODE_VOLUME_DOWN) return super.dispatchKeyEvent(event); int keysym = androidKeyToXKeysym(keyCode); @@ -540,10 +547,26 @@ protected static boolean isModifierKey(int keyCode) { protected void toggleSoftKeyboard() { var imm = getSystemService(InputMethodManager.class); - if (imm != null && ivDisplay != null) { - ivDisplay.requestFocus(); - imm.showSoftInput(ivDisplay, 0); - } + if (imm == null || ivDisplay == null) return; + // Post so the fab-menu popup has finished tearing down: called inline right after the item + // click, the popup still owns the focus transition and showSoftInput lands before ivDisplay + // is the served view and does nothing. (The letterbox onClick path already has focus, but + // routing both through the same retry keeps them consistent.) + mainHandler.post(() -> tryShowKeyboard(imm, 15)); + } + + // showSoftInput() can return true for a view the IMM isn't serving yet and show nothing, so the + // success test is imm.isActive(view), retried on a short delay until the input connection is + // live. The last few rounds force the IME (some ROMs ignore the implicit request). + private void tryShowKeyboard(@NonNull InputMethodManager imm, int attemptsLeft) { + if (attemptsLeft <= 0 || isFinishing() || ivDisplay == null) return; + ivDisplay.requestFocusFromTouch(); + ivDisplay.requestFocus(); + int flag = attemptsLeft <= 3 + ? InputMethodManager.SHOW_FORCED : InputMethodManager.SHOW_IMPLICIT; + imm.showSoftInput(ivDisplay, flag); + if (ivDisplay.isFocused() && imm.isActive(ivDisplay)) return; + mainHandler.postDelayed(() -> tryShowKeyboard(imm, attemptsLeft - 1), 60); } protected VncDisplayView.TextCommitListener createTextCommitListener() { @@ -727,10 +750,7 @@ protected void reconnect() { protected boolean onMenuItemClicked(@NonNull MenuItem item) { int id = item.getItemId(); - if (id == R.id.menu_keyboard) { - toggleSoftKeyboard(); - return true; - } else if (id == R.id.menu_rotate) { + if (id == R.id.menu_rotate) { rotateScreen(); return true; } else if (id == R.id.menu_reconnect) { diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/display/VMVncDisplayActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/display/VMVncDisplayActivity.java index 135310e7d..ff39f1f1c 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/display/VMVncDisplayActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/display/VMVncDisplayActivity.java @@ -20,6 +20,7 @@ import android.content.Context; import android.content.SharedPreferences; import android.graphics.Bitmap; +import android.graphics.RectF; import android.graphics.drawable.GradientDrawable; import android.util.TypedValue; import android.view.MenuItem; @@ -36,68 +37,199 @@ import androidx.annotation.NonNull; import androidx.core.graphics.Insets; import androidx.core.view.ViewCompat; +import androidx.core.view.WindowCompat; import androidx.core.view.WindowInsetsCompat; import com.google.android.material.button.MaterialButton; import com.google.android.material.floatingactionbutton.FloatingActionButton; +import org.json.JSONObject; + import cn.classfun.droidvm.R; +import cn.classfun.droidvm.display.INativeDisplayRootService; +import cn.classfun.droidvm.lib.daemon.DaemonConnection; import cn.classfun.droidvm.lib.ui.DragTouchListener; import cn.classfun.droidvm.lib.ui.MaterialMenu; +import cn.classfun.droidvm.ui.vm.display.base.DaemonDisplayAttach; +import cn.classfun.droidvm.ui.vm.display.base.DisplayChromeController; +import cn.classfun.droidvm.ui.vm.display.base.DisplayExtraKeysPanel; +import cn.classfun.droidvm.ui.vm.display.base.DisplayKeyboardMenuRow; +import cn.classfun.droidvm.ui.vm.display.base.KeyboardMode; +import cn.classfun.droidvm.ui.vm.display.base.DisplayPhysicalKeyboardView; +import cn.classfun.droidvm.ui.vm.display.base.DisplaySource; +import cn.classfun.droidvm.ui.vm.display.base.DisplayViewportController; +import cn.classfun.droidvm.ui.vm.display.base.InputMode; +import cn.classfun.droidvm.ui.vm.display.base.PointerGestureTranslator; +import cn.classfun.droidvm.ui.vm.display.nativedisplay.input.DirectInputSink; +import cn.classfun.droidvm.ui.vm.display.nativedisplay.input.EvdevEncoder; +import cn.classfun.droidvm.ui.vm.display.nativedisplay.input.InputForwarder; import cn.classfun.droidvm.ui.vm.display.vnc.base.BaseVncActivity; public final class VMVncDisplayActivity extends BaseVncActivity { - private static final long AUTO_HIDE_DELAY_MS = 3000; private static final long OP_LABEL_HIDE_DELAY_MS = 2000; private static final String PREFS_NAME = "droidvm_prefs"; private static final String KEY_INPUT_MODE = "display_input_mode"; - private static final float TAP_SLOP = 20f; - private static final long TAP_TIMEOUT = 250; - private static final long DOUBLE_TAP_TIMEOUT = 300; - private static final float DEFAULT_ZOOM = 1f; - private static final float MIN_ZOOM = 0.5f; - private static final float MAX_ZOOM = 5f; - private static final float SNAP_THRESHOLD = 15f; - private static final float MIN_SCALE_DIST = 24f; - private static final float SCROLL_THRESHOLD = 8f; + // Chrome memory, shared with the native path: extra-keys on/off per typing surface + whether + // the physical keyboard is up. + private static final String KEY_KEYBOARD_MODE = "display_keyboard_mode"; + private static final String KEY_ZONE_EXTRA = "display_keyboard_zone_extra"; + private static final String KEY_ZONE_FNX = "display_keyboard_zone_fnx"; + // RFB pointer button-mask bits (the crosvm VNC server is fixed to tablet mode: an absolute + // pointer with these buttons plus scroll pulses). private static final int MASK_LEFT = 1; private static final int MASK_MIDDLE = 2; private static final int MASK_RIGHT = 4; private static final int MASK_SCROLL_UP = 8; private static final int MASK_SCROLL_DOWN = 16; - private enum InputMode {TOUCH, MOUSE} private LinearLayout statusBar; private MaterialButton btnFullscreen; private FrameLayout displayContainer; private FloatingActionButton fabMenu; private TextView operationLabel; - private boolean isFullscreen = false; - private boolean extraKeysVisible = true; + private DisplayPhysicalKeyboardView phyKeyboard; + // Per-mode input routing: whatever the VNC channel natively has goes over RFB (TABLET's + // absolute pointer + the keyboard); the rest goes to the crosvm --input evdev devices via the + // daemon (MOUSE = relative motion the guest renders a cursor for, TOUCH = raw multi-touch). + // Seeded from the shared pref in onSetupActivity(); TOUCH only until that read. private InputMode inputMode = InputMode.TOUCH; private SharedPreferences prefs; - private float cursorX, cursorY; - private int baseViewW, baseViewH; - private float zoom = DEFAULT_ZOOM; - private float panX, panY; - private int gestureMaxPointers; - private boolean gestureMoved; - private long gestureStartTime; - private float gestureStartMidX, gestureStartMidY; - private long lastTapTime; - private int lastTapFingerCount; - private float lastTouchX, lastTouchY; - private float lastMidX, lastMidY; - private float initialAngle; - private float rotationBase; - private float initialDist; - private float initialZoom; - private float lastScrollMidY; + + private InputForwarder inputForwarder; + private PointerGestureTranslator gestureTranslator; + private int rfbMask; // current RFB button mask (tablet mode) + private int rfbLastX, rfbLastY; // last absolute pointer position sent, fb px + private float mouseRemX, mouseRemY; // fractional remainders of relative mouse motion + // Single sources of truth for viewport geometry (fit/zoom/pan across display-area changes) + // and chrome visibility (fullscreen / extra keys). See the controller classes for the rules. + private DisplayViewportController viewport; + private DisplayChromeController chrome; + // Display areas smaller than this (e.g. landscape with a tall IME) freeze the viewport + // instead of re-laying it out; see DisplayViewportController. + private static final int MIN_AREA_DP = 96; + // Last mouse right/middle-button activity: any BACK key arriving shortly after is the + // framework's (or OEM's) right-click fallback and must not navigate away from the VM. + private long lastMouseButtonMs; + private static final long MOUSE_BACK_SUPPRESS_MS = 800; + + // Daemon broker binder plumbing (shared DaemonDisplayAttach): the direct sink turns each + // evdev frame into one binder call instead of the (much slower) vm_input JSON-RPC round-trip. + // Input works immediately on the RPC fallback and upgrades in place once the binder arrives; + // written on the main thread, read on the InputForwarder worker (hence volatile). + private volatile DirectInputSink directSink; + private DaemonDisplayAttach displayAttach; + // Display-source adapter: framebuffer events flow through the shared DisplaySource interface. + private VncBitmapSource displaySource; private final Runnable hideOperationLabel = () -> { if (operationLabel != null) operationLabel.setVisibility(GONE); }; + // absolute-tablet pointer); MOUSE lands on the crosvm relative-mouse device via vm_input. + private final PointerGestureTranslator.Listener gestureListener = + new PointerGestureTranslator.Listener() { + @Override + public void onRelativeMove(float dxGuest, float dyGuest) { + if (inputForwarder == null) return; + mouseRemX += dxGuest; + mouseRemY += dyGuest; + int dx = (int) mouseRemX, dy = (int) mouseRemY; + if (dx == 0 && dy == 0) return; + mouseRemX -= dx; + mouseRemY -= dy; + inputForwarder.sendMouseMove(dx, dy); + } + + @Override + public void onAbsoluteMove(float xGuest, float yGuest) { + rfbMove(Math.round(xGuest), Math.round(yGuest)); + } + + @Override + public void onLeftButton(boolean down, float xGuest, float yGuest) { + if (inputMode == InputMode.TABLET) { + rfbButton(MASK_LEFT, down, Math.round(xGuest), Math.round(yGuest)); + } else if (inputForwarder != null) { + inputForwarder.sendPointerButton(EvdevEncoder.BTN_LEFT, down); + } + } + + @Override + public void onLeftTap(float xGuest, float yGuest) { + onLeftButton(true, xGuest, yGuest); + onLeftButton(false, xGuest, yGuest); + } + + @Override + public void onRightClick(float xGuest, float yGuest) { + if (inputMode == InputMode.TABLET) { + int x = Math.round(xGuest), y = Math.round(yGuest); + rfbButton(MASK_RIGHT, true, x, y); + rfbButton(MASK_RIGHT, false, x, y); + } else if (inputForwarder != null) { + inputForwarder.sendPointerButton(EvdevEncoder.BTN_RIGHT, true); + inputForwarder.sendPointerButton(EvdevEncoder.BTN_RIGHT, false); + } + } + + @Override + public void onScroll(int vNotches, int hNotches) { + if (inputMode == InputMode.TABLET) { + rfbScroll(vNotches); + } else if (inputForwarder != null) { + inputForwarder.sendScroll(vNotches, hNotches); + } + } + + @Override + public void onZoomPan(float scaleFactor, float dxView, float dyView, + float focusX, float focusY) { + if (viewport != null) viewport.onZoomPan(scaleFactor, dxView, dyView); + } + }; + + // ---- RFB tablet-pointer helpers (absolute position + button mask) ---- + + private void rfbMove(int x, int y) { + if (vncClient == null || !vncClient.isConnected() || fbWidth <= 0) return; + rfbLastX = max(0, min(x, fbWidth - 1)); + rfbLastY = max(0, min(y, fbHeight - 1)); + vncClient.sendPointer(rfbLastX, rfbLastY, rfbMask); + } + + private void rfbButton(int maskBit, boolean down, int x, int y) { + if (vncClient == null || !vncClient.isConnected() || fbWidth <= 0) return; + rfbLastX = max(0, min(x, fbWidth - 1)); + rfbLastY = max(0, min(y, fbHeight - 1)); + rfbMask = down ? (rfbMask | maskBit) : (rfbMask & ~maskBit); + vncClient.sendPointer(rfbLastX, rfbLastY, rfbMask); + } + + /** RFB has no wheel axis; each notch is a scroll-button press/release pulse. */ + private void rfbScroll(int vNotches) { + if (vncClient == null || !vncClient.isConnected() || vNotches == 0) return; + int bit = vNotches > 0 ? MASK_SCROLL_UP : MASK_SCROLL_DOWN; + for (int i = 0; i < Math.abs(vNotches); i++) { + vncClient.sendPointer(rfbLastX, rfbLastY, rfbMask | bit); + vncClient.sendPointer(rfbLastX, rfbLastY, rfbMask); + } + } + + // Ships evdev records for MOUSE/TOUCH modes to the daemon, which owns the crosvm --input + // sockets. Runs on the InputForwarder worker thread (synchronous request keeps ordering). + private boolean sendInputToDaemon(int channel, @NonNull byte[] data) { + try { + var req = new JSONObject(); + req.put("command", "vm_input"); + req.put("vm_id", vmId); + req.put("channel", channel); + req.put("data", android.util.Base64.encodeToString(data, android.util.Base64.NO_WRAP)); + var resp = DaemonConnection.getInstance().request(req); + return resp.optBoolean("delivered", false); + } catch (Exception e) { + return false; + } + } @Override protected int getContentLayoutId() { @@ -116,25 +248,165 @@ protected void onBindExtraViews() { displayContainer = findViewById(R.id.display_container); fabMenu = findViewById(R.id.fab_menu); operationLabel = findViewById(R.id.tv_operation); + phyKeyboard = findViewById(R.id.phy_keyboard); } @Override protected void onSetupActivity() { prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); - inputMode = InputMode.values()[prefs.getInt(KEY_INPUT_MODE, 0)]; + inputMode = InputMode.fromOrdinal(prefs.getInt(KEY_INPUT_MODE, 0)); + phyKeyboard.setKeyListener(vncExtraKeys); + // The physical keyboard's Shift/Ctrl/Alt/Win mirror the panel's sticky-modifier state. + extraKeysPanel.setModifierStateObserver(() -> phyKeyboard.refreshModifiers( + extraKeysPanel.isCtrlDown(), extraKeysPanel.isAltDown(), + extraKeysPanel.isShiftDown(), extraKeysPanel.isWinDown())); + extraKeysPanel.setZoneListener(new DisplayExtraKeysPanel.ZoneListener() { + @Override + public void onToggleFnxZone() { + chrome.toggleFnxZone(); + } + + @Override + public void onShowSystemKeyboard() { + toggleSoftKeyboard(); + } + }); + phyKeyboard.setZoneListener(new DisplayPhysicalKeyboardView.ZoneListener() { + @Override + public void onToggleExtraZone() { + chrome.toggleExtraZone(); + } + + @Override + public void onToggleFnxZone() { + chrome.toggleFnxZone(); + } + + @Override + public void onCloseKeyboard() { + chrome.setKeyboardMode(KeyboardMode.NONE); + } + }); setupCutoutMode(); - setupWindowInsets(); + setupLayoutControllers(); btnFullscreen.setOnClickListener(v -> toggleFullscreen()); + // The container's layout size IS the display area: chrome visibility, IME and rotation + // all funnel into it through normal layout. The viewport handles degenerate sizes itself. displayContainer.addOnLayoutChangeListener(( v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom ) -> { int cw = right - left, ch = bottom - top; - if (cw > 0 && ch > 0) v.post(() -> updateAspectRatio(cw, ch)); + v.post(() -> viewport.setArea(cw, ch)); }); setupOperationLabel(); setupDisplayTouch(); setupFab(); + // Delegating sink: the direct daemon-binder path once attached, the vm_input JSON-RPC + // until then (and again if the binder dies). DirectInputSink falls back internally on + // any per-write failure, so input never drops during the upgrade. + inputForwarder = new InputForwarder((channel, data) -> { + var sink = directSink; + return sink != null ? sink.write(channel, data) : sendInputToDaemon(channel, data); + }); + gestureTranslator = new PointerGestureTranslator(mainHandler, gestureListener); + // Hardware mouse/stylus: hover is TABLET-only (RFB absolute move); wheel and right/middle + // buttons route per mode (tablet -> RFB mask, mouse -> crosvm mouse device). + ivDisplay.setOnHoverListener(this::onDisplayHover); + ivDisplay.setOnGenericMotionListener(this::onDisplayGenericMotion); + // Also on the container: a right-click over the letterbox area (outside the image) must + // still be consumed or the framework synthesizes BACK from it. + displayContainer.setOnGenericMotionListener(this::onDisplayGenericMotion); + // Keep the display area out of the system-gesture zones so multi-finger gestures + // (two-finger right-click/scroll, three-finger zoom) don't trip OEM gestures. + displayContainer.addOnLayoutChangeListener((v, l, t, r, b, ol, ot, or2, ob) -> + v.setSystemGestureExclusionRects(java.util.Collections.singletonList( + new android.graphics.Rect(0, 0, r - l, b - t)))); applyInputMode(); + + if (!vmId.isEmpty()) { + displayAttach = new DaemonDisplayAttach(this, mainHandler, + new DaemonDisplayAttach.Listener() { + @Override + public void onAttached(@NonNull INativeDisplayRootService service) { + } + + @Override + public void onLost() { + // Drop the direct sink so writes go back to the vm_input RPC. + directSink = null; + } + }); + displayAttach.start(); + } + } + + private boolean onDisplayHover(View v, MotionEvent event) { + if (inputMode != InputMode.TABLET) return false; + if (fbWidth <= 0 || v.getWidth() <= 0 || v.getHeight() <= 0) return false; + int action = event.getActionMasked(); + if (action == MotionEvent.ACTION_HOVER_MOVE || action == MotionEvent.ACTION_HOVER_ENTER) { + rfbMove(Math.round(event.getX() * fbWidth / v.getWidth()), + Math.round(event.getY() * fbHeight / v.getHeight())); + return true; + } + return false; + } + + // Button presses are ALWAYS consumed (every mode, letterbox included) - an unhandled + // BUTTON_SECONDARY press is what makes the framework synthesize a BACK key. + private boolean onDisplayGenericMotion(View v, MotionEvent event) { + switch (event.getActionMasked()) { + case MotionEvent.ACTION_SCROLL: { + int vN = Math.round(event.getAxisValue(MotionEvent.AXIS_VSCROLL)); + int hN = Math.round(event.getAxisValue(MotionEvent.AXIS_HSCROLL)); + if (inputMode == InputMode.TABLET) rfbScroll(vN); + else if (inputForwarder != null) inputForwarder.sendScroll(vN, hN); + return true; + } + case MotionEvent.ACTION_BUTTON_PRESS: + case MotionEvent.ACTION_BUTTON_RELEASE: { + lastMouseButtonMs = android.os.SystemClock.uptimeMillis(); + boolean down = event.getActionMasked() == MotionEvent.ACTION_BUTTON_PRESS; + // Map view coords to fb px; events from the container carry the letterbox offset. + float lx = event.getX(), ly = event.getY(); + if (v == displayContainer) { + lx -= ivDisplay.getLeft(); + ly -= ivDisplay.getTop(); + } + int ivW = ivDisplay.getWidth(), ivH = ivDisplay.getHeight(); + int x = ivW > 0 && fbWidth > 0 ? Math.round(lx * fbWidth / ivW) : rfbLastX; + int y = ivH > 0 && fbHeight > 0 ? Math.round(ly * fbHeight / ivH) : rfbLastY; + switch (event.getActionButton()) { + case MotionEvent.BUTTON_SECONDARY: + case MotionEvent.BUTTON_STYLUS_PRIMARY: + if (inputMode == InputMode.TABLET) rfbButton(MASK_RIGHT, down, x, y); + else if (inputForwarder != null) + inputForwarder.sendPointerButton(EvdevEncoder.BTN_RIGHT, down); + break; + case MotionEvent.BUTTON_TERTIARY: + if (inputMode == InputMode.TABLET) rfbButton(MASK_MIDDLE, down, x, y); + else if (inputForwarder != null) + inputForwarder.sendPointerButton(EvdevEncoder.BTN_MIDDLE, down); + break; + default: + break; + } + return true; + } + default: + return false; + } + } + + @Override + public boolean dispatchKeyEvent(@NonNull android.view.KeyEvent event) { + // OEM-injected right-click fallback BACK may claim a keyboard/virtual source, which the + // base class's mouse-source check misses; the timestamp catches it regardless. + if (event.getKeyCode() == android.view.KeyEvent.KEYCODE_BACK + && android.os.SystemClock.uptimeMillis() - lastMouseButtonMs + < MOUSE_BACK_SUPPRESS_MS) + return true; + return super.dispatchKeyEvent(event); } private void setupCutoutMode() { @@ -144,13 +416,78 @@ private void setupCutoutMode() { getWindow().setAttributes(params); } - private void setupWindowInsets() { + // Wires the viewport controller (single writer of the display-image geometry), the chrome + // controller (single writer of toolbar/status bar/extra keys/system bars visibility) and the + // window-insets listener that turns system bars + IME into content padding, which in turn + // sizes the display container. + private void setupLayoutControllers() { + int minAreaPx = Math.round(MIN_AREA_DP * getResources().getDisplayMetrics().density); + viewport = new DisplayViewportController(minAreaPx, + new DisplayViewportController.Listener() { + @Override + public void onViewportChanged(int baseW, int baseH, float viewScale, + float offsetX, float offsetY) { + } + + @Override + public void onGuestResizeWanted(int areaW, int areaH) { + // Auto-resize Guest Display: no guest-side channel on this path yet. + } + }); + + displaySource = new VncBitmapSource(new DisplaySource.Callbacks() { + @Override + public void onContentSize(int width, int height) { + viewport.setContentSize(width, height); + } + + @Override + public void onStateChanged(@NonNull DisplaySource.State state) { + // Status text and overlay are handled by BaseVncActivity's own status plumbing. + } + }); + + chrome = new DisplayChromeController( + KeyboardMode.fromName(prefs.getString(KEY_KEYBOARD_MODE, null)), + prefs.getBoolean(KEY_ZONE_EXTRA, true), + prefs.getBoolean(KEY_ZONE_FNX, false), + (fullscreen, mode, extraVisible, fnxVisible) -> { + toolbar.setVisibility(fullscreen ? GONE : VISIBLE); + statusBar.setVisibility(fullscreen ? GONE : VISIBLE); + extraKeysPanel.applyZones( + extraVisible, fnxVisible, mode == KeyboardMode.SYSTEM); + phyKeyboard.setZoneToggleState(extraVisible, fnxVisible); + phyKeyboard.setVisibleAnimated(mode == KeyboardMode.LAPTOP); + var controller = getWindow().getInsetsController(); + if (controller != null) { + if (fullscreen) { + controller.hide(WindowInsets.Type.systemBars()); + controller.setSystemBarsBehavior(BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); + } else { + controller.show(WindowInsets.Type.systemBars()); + } + } + // Re-request insets so the content padding (and thus the display area) updates in + // the same pass as the visibility changes. + ViewCompat.requestApplyInsets(findViewById(android.R.id.content)); + }); + chrome.setStateListener((mode, extraVisible, fnxVisible) -> prefs.edit() + .putString(KEY_KEYBOARD_MODE, mode.name()) + .putBoolean(KEY_ZONE_EXTRA, extraVisible) + .putBoolean(KEY_ZONE_FNX, fnxVisible) + .apply()); + chrome.applyInitial(); + var content = (ViewGroup) findViewById(android.R.id.content); ViewCompat.setOnApplyWindowInsetsListener(content, (v, insets) -> { Insets sysBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); Insets ime = insets.getInsets(WindowInsetsCompat.Type.ime()); - int top = isFullscreen ? 0 : sysBars.top; - int bottom = ime.bottom; + // The Main row's shared slot follows the IME: Fn while it is up, "show IME" while + // it is down. The system owns that visibility, so read it rather than track it. + extraKeysPanel.setImeVisible(insets.isVisible(WindowInsetsCompat.Type.ime())); + boolean fullscreen = chrome != null && chrome.isFullscreen(); + int top = fullscreen ? 0 : sysBars.top; + int bottom = Math.max(fullscreen ? 0 : sysBars.bottom, ime.bottom); v.setPadding(0, top, 0, bottom); return insets; }); @@ -172,12 +509,7 @@ private void setupOperationLabel() { @Override protected void onFramebufferReady(int width, int height) { - updateAspectRatio(displayContainer.getWidth(), displayContainer.getHeight()); - if (inputMode == InputMode.MOUSE) { - if (cursorX < 0 || cursorX >= width) cursorX = width / 2f; - if (cursorY < 0 || cursorY >= height) cursorY = height / 2f; - ensureCursorVisible(); - } + displaySource.dispatchContentSize(width, height); } @Override @@ -185,56 +517,65 @@ protected void onBitmapUpdated(@NonNull Bitmap bitmap) { ivDisplay.setImageBitmap(bitmap); } - @Override - protected void onStatusChanged(String text, VncStatus status) { - mainHandler.removeCallbacks(this::hideBars); - if (!isFullscreen) showBars(); - } - @Override protected void onDestroyExtra() { - mainHandler.removeCallbacks(this::hideBars); mainHandler.removeCallbacks(hideOperationLabel); + if (inputForwarder != null) inputForwarder.close(); + var sink = directSink; + directSink = null; + if (sink != null) sink.close(); + if (displayAttach != null) { + displayAttach.stop(); + displayAttach = null; + } } + // Routes on-screen touches by input mode. TOUCH listens on ivDisplay, which is laid out to + // the framebuffer's aspect (DisplayViewportController), so view coords scale straight to the + // wire range. MOUSE/TABLET listen on the whole container; the gesture translator pins the + // coordinate-carrying gestures to the rendered image rect. private boolean onDisplayTouch(View v, MotionEvent event) { - if (vncClient == null || !vncClient.isConnected()) return false; if (fbWidth <= 0 || fbHeight <= 0) return false; - float viewX = event.getX(), viewY = event.getY(); - float ivW = v.getWidth(), ivH = v.getHeight(); - float imgAspect = (float) fbWidth / fbHeight; - float viewAspect = ivW / max(ivH, 1); - float drawnW, drawnH, offsetX, offsetY; - if (imgAspect > viewAspect) { - drawnW = ivW; - drawnH = ivW / imgAspect; - offsetX = 0; - offsetY = (ivH - drawnH) / 2; - } else { - drawnH = ivH; - drawnW = ivH * imgAspect; - offsetX = (ivW - drawnW) / 2; - offsetY = 0; + int ivW = ivDisplay.getWidth(), ivH = ivDisplay.getHeight(); + if (ivW <= 0 || ivH <= 0) return false; + // A hardware-mouse right/middle press also arrives on the touch stream (ACTION_DOWN with + // the button in buttonState). Those are delivered by the generic-motion handler; keep them + // out of the tap/gesture path (else right-click doubles as a left tap) but consume them so + // the framework doesn't synthesize a BACK key from an unhandled right-click. + if ((event.getSource() & android.view.InputDevice.SOURCE_MOUSE) != 0 + && (event.getButtonState() & (MotionEvent.BUTTON_SECONDARY + | MotionEvent.BUTTON_TERTIARY | MotionEvent.BUTTON_STYLUS_PRIMARY)) != 0) { + lastMouseButtonMs = android.os.SystemClock.uptimeMillis(); + return true; } - int vncX = (int) ((viewX - offsetX) / drawnW * fbWidth); - int vncY = (int) ((viewY - offsetY) / drawnH * fbHeight); - vncX = max(0, min(vncX, fbWidth - 1)); - vncY = max(0, min(vncY, fbHeight - 1)); - int mask; - switch (event.getActionMasked()) { - case MotionEvent.ACTION_DOWN: - case MotionEvent.ACTION_MOVE: - mask = 1; - break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - mask = 0; - break; + switch (inputMode) { + case TABLET: + case MOUSE: + // TABLET rides RFB, whose pointer coordinates are framebuffer px (crosvm's VNC + // server normalizes them itself); MOUSE REL deltas are guest px. Both therefore + // map the display rect onto the framebuffer size, NOT the normalized ABS range. + return gestureTranslator != null && gestureTranslator.onTouchEvent( + event, displayRectInContainer(), fbWidth, fbHeight); + case TOUCH: default: - return false; + if (inputForwarder == null) return false; + // The evdev multi-touch device advertises the fixed normalized ABS range + // (--input multi-touch with no width/height); scale view coords to that range. + inputForwarder.sendTouchEvent(event, + (float) EvdevEncoder.NORMALIZED_ABS_MAX / ivW, + (float) EvdevEncoder.NORMALIZED_ABS_MAX / ivH); + return true; } - vncClient.sendPointer(vncX, vncY, mask); - return true; + } + + // Where the framebuffer is rendered, in container coordinates: the letterbox-fitted image + // bounds mapped through the viewport's current zoom/pan transform. + @NonNull + private RectF displayRectInContainer() { + var rect = new RectF(0, 0, ivDisplay.getWidth(), ivDisplay.getHeight()); + ivDisplay.getMatrix().mapRect(rect); + rect.offset(ivDisplay.getLeft(), ivDisplay.getTop()); + return rect; } @SuppressLint("ClickableViewAccessibility") @@ -244,359 +585,44 @@ private void setupDisplayTouch() { @SuppressLint("ClickableViewAccessibility") private void applyInputMode() { - if (inputMode == InputMode.MOUSE) { - ivDisplay.setScaleType(ImageView.ScaleType.FIT_CENTER); + ivDisplay.setScaleType(ImageView.ScaleType.FIT_CENTER); + if (operationLabel != null) operationLabel.setVisibility(GONE); + if (inputMode == InputMode.MOUSE || inputMode == InputMode.TABLET) { + // Whole-container gesture surface: MOUSE is a borderless touchpad; TABLET pins the + // coordinate-carrying gestures to the rendered image inside the translator, so + // multi-finger gestures still work from the letterbox. ivDisplay.setClickable(false); ivDisplay.setOnTouchListener(null); displayContainer.setClickable(true); displayContainer.setOnClickListener(null); - displayContainer.setOnTouchListener(this::onMouseTouch); - if (operationLabel != null) operationLabel.setVisibility(GONE); - applyViewSize(); - applyViewTransform(); - ensureCursorVisible(); + displayContainer.setOnTouchListener(this::onDisplayTouch); } else { - ivDisplay.setScaleType(ImageView.ScaleType.FIT_CENTER); ivDisplay.setClickable(true); ivDisplay.setOnTouchListener(this::onDisplayTouch); displayContainer.setOnTouchListener(null); displayContainer.setClickable(true); + // Tap-to-summon-IME, unless the physical keyboard is the active typing surface. displayContainer.setOnClickListener(v -> { - showBars(); - toggleSoftKeyboard(); + if (chrome == null || chrome.getKeyboardMode() != KeyboardMode.LAPTOP) + toggleSoftKeyboard(); }); - if (operationLabel != null) operationLabel.setVisibility(GONE); } + if (gestureTranslator != null) { + gestureTranslator.setAbsolute(inputMode == InputMode.TABLET); + gestureTranslator.reset(); + } + if (inputForwarder != null) + inputForwarder.setInputMode(inputMode); } private void setInputMode(InputMode mode) { if (inputMode == mode) return; inputMode = mode; prefs.edit().putInt(KEY_INPUT_MODE, mode.ordinal()).apply(); - resetViewTransform(); - if (mode == InputMode.MOUSE) { - if (fbWidth > 0) cursorX = fbWidth / 2f; - if (fbHeight > 0) cursorY = fbHeight / 2f; - } - applyViewSize(); + viewport.resetToFit(); applyInputMode(); } - private void resetViewTransform() { - zoom = DEFAULT_ZOOM; - panX = 0; - panY = 0; - ivDisplay.setTranslationX(0); - ivDisplay.setTranslationY(0); - ivDisplay.setRotation(0); - } - - private int currentViewW() { - return inputMode == InputMode.MOUSE - ? Math.round(baseViewW * zoom) : baseViewW; - } - - private int currentViewH() { - return inputMode == InputMode.MOUSE - ? Math.round(baseViewH * zoom) : baseViewH; - } - - private void applyViewSize() { - if (baseViewW <= 0 || baseViewH <= 0) return; - int w = currentViewW(), h = currentViewH(); - var lp = ivDisplay.getLayoutParams(); - if (lp instanceof FrameLayout.LayoutParams) { - ((FrameLayout.LayoutParams) lp).gravity = CENTER; - lp.width = w; - lp.height = h; - } else { - lp = new FrameLayout.LayoutParams(w, h, CENTER); - } - ivDisplay.setLayoutParams(lp); - } - - private void applyViewTransform() { - ivDisplay.setTranslationX(panX); - ivDisplay.setTranslationY(panY); - } - - private float midX(@NonNull MotionEvent e) { - float s = 0; - for (int i = 0; i < e.getPointerCount(); i++) s += e.getX(i); - return s / e.getPointerCount(); - } - - private float midY(@NonNull MotionEvent e) { - float s = 0; - for (int i = 0; i < e.getPointerCount(); i++) s += e.getY(i); - return s / e.getPointerCount(); - } - - @SuppressLint("ClickableViewAccessibility") - private boolean onMouseTouch(View v, MotionEvent event) { - if (vncClient == null || !vncClient.isConnected()) return false; - if (fbWidth <= 0 || fbHeight <= 0) return false; - int pc = event.getPointerCount(); - switch (event.getActionMasked()) { - case MotionEvent.ACTION_DOWN: - gestureMaxPointers = 1; - gestureMoved = false; - gestureStartTime = System.currentTimeMillis(); - gestureStartMidX = event.getX(); - gestureStartMidY = event.getY(); - lastTouchX = event.getX(); - lastTouchY = event.getY(); - return true; - case MotionEvent.ACTION_POINTER_DOWN: - gestureMaxPointers = max(gestureMaxPointers, pc); - // Re-anchor the tap reference to the multi-finger midpoint and - // restart the tap window. The ACTION_DOWN anchor was a single - // finger position, so the two-finger midpoint sits ~half a - // finger-spread away and would instantly trip gestureMoved, - // making a still two-finger tap (right click) impossible. - gestureStartMidX = midX(event); - gestureStartMidY = midY(event); - gestureStartTime = System.currentTimeMillis(); - gestureMoved = false; - if (pc == 2) initTwoFinger(event); - if (pc >= 3) lastScrollMidY = midY(event); - return true; - case MotionEvent.ACTION_MOVE: { - float mx = midX(event), my = midY(event); - float dist = (float) Math.hypot( - mx - gestureStartMidX, my - gestureStartMidY); - if (dist > TAP_SLOP) gestureMoved = true; - if (gestureMaxPointers >= 3 && pc >= 3) { - float dy = my - lastScrollMidY; - if (Math.abs(dy) > SCROLL_THRESHOLD) { - boolean up = dy < 0; - vncClient.sendPointer((int) cursorX, (int) cursorY, - up ? MASK_SCROLL_UP : MASK_SCROLL_DOWN); - vncClient.sendPointer((int) cursorX, (int) cursorY, 0); - showOperation(up - ? R.string.vnc_op_scroll_up - : R.string.vnc_op_scroll_down); - lastScrollMidY = my; - } - } else if (gestureMaxPointers >= 2 && pc >= 2) { - handleTwoFinger(event); - } else if (gestureMaxPointers <= 1 && pc == 1) { - float dx = event.getX() - lastTouchX; - float dy = event.getY() - lastTouchY; - lastTouchX = event.getX(); - lastTouchY = event.getY(); - if (dx != 0 || dy != 0) { - moveCursor(dx, dy); - showOperation(R.string.vnc_op_mouse_move); - } - } - return true; - } - case MotionEvent.ACTION_UP: - if (gestureMaxPointers >= 2) { - ivDisplay.setRotation(snapRotation(ivDisplay.getRotation())); - clampPan(); - ivDisplay.setTranslationX(panX); - ivDisplay.setTranslationY(panY); - updateOperationLabelPosition(); - } else { - ensureCursorVisible(); - } - if (!gestureMoved - && System.currentTimeMillis() - gestureStartTime < TAP_TIMEOUT) { - boolean dbl = lastTapFingerCount == gestureMaxPointers - && System.currentTimeMillis() - lastTapTime < DOUBLE_TAP_TIMEOUT; - handleTap(gestureMaxPointers, dbl); - lastTapTime = System.currentTimeMillis(); - lastTapFingerCount = gestureMaxPointers; - } else { - lastTapFingerCount = 0; - } - gestureMaxPointers = 0; - return true; - case MotionEvent.ACTION_CANCEL: - gestureMaxPointers = 0; - lastTapFingerCount = 0; - return true; - case MotionEvent.ACTION_POINTER_UP: - if (gestureMaxPointers == 2 && pc <= 2) - ivDisplay.setRotation(snapRotation(ivDisplay.getRotation())); - return true; - } - return false; - } - - private void handleTap(int fingerCount, boolean dbl) { - switch (fingerCount) { - case 1: - sendClick(MASK_LEFT); - if (dbl) sendClick(MASK_LEFT); - showOperation(dbl - ? R.string.vnc_op_left_double_click - : R.string.vnc_op_left_click); - break; - case 2: - sendClick(MASK_RIGHT); - if (dbl) sendClick(MASK_RIGHT); - showOperation(dbl - ? R.string.vnc_op_right_double_click - : R.string.vnc_op_right_click); - break; - default: - sendClick(MASK_MIDDLE); - if (dbl) sendClick(MASK_MIDDLE); - showOperation(dbl - ? R.string.vnc_op_middle_double_click - : R.string.vnc_op_middle_click); - break; - } - } - - private void sendClick(int mask) { - vncClient.sendPointer((int) cursorX, (int) cursorY, mask); - vncClient.sendPointer((int) cursorX, (int) cursorY, 0); - } - - private void initTwoFinger(@NonNull MotionEvent event) { - lastMidX = (event.getX(0) + event.getX(1)) / 2f; - lastMidY = (event.getY(0) + event.getY(1)) / 2f; - initialAngle = twoFingerAngle(event); - rotationBase = ivDisplay.getRotation(); - initialDist = twoFingerDistance(event); - initialZoom = zoom; - } - - private void handleTwoFinger(@NonNull MotionEvent event) { - float mx = (event.getX(0) + event.getX(1)) / 2f; - float my = (event.getY(0) + event.getY(1)) / 2f; - panX += mx - lastMidX; - panY += my - lastMidY; - lastMidX = mx; - lastMidY = my; - clampPan(); - ivDisplay.setTranslationX(panX); - ivDisplay.setTranslationY(panY); - float angle = twoFingerAngle(event); - float dAngle = normalizeAngle(angle - initialAngle); - ivDisplay.setRotation(snapNear(rotationBase + dAngle)); - float dist = twoFingerDistance(event); - if (initialDist > MIN_SCALE_DIST) { - float nz = max(MIN_ZOOM, min(initialZoom * dist / initialDist, MAX_ZOOM)); - if (nz != zoom) { - zoom = nz; - applyViewSize(); - } - } - updateOperationLabelPosition(); - } - - private void clampPan() { - int cW = displayContainer.getWidth(); - int cH = displayContainer.getHeight(); - int vW = currentViewW(); - int vH = currentViewH(); - panX = max(-(cW + vW) / 2f, min(panX, (cW + vW) / 2f)); - panY = max(-(cH + vH) / 2f, min(panY, (cH + vH) / 2f)); - } - - private float twoFingerAngle(@NonNull MotionEvent e) { - float dx = e.getX(1) - e.getX(0); - float dy = e.getY(1) - e.getY(0); - return (float) Math.toDegrees(Math.atan2(dy, dx)); - } - - private float twoFingerDistance(@NonNull MotionEvent e) { - float dx = e.getX(1) - e.getX(0); - float dy = e.getY(1) - e.getY(0); - return (float) Math.hypot(dx, dy); - } - - private float normalizeAngle(float a) { - while (a > 180) a -= 360; - while (a < -180) a += 360; - return a; - } - - private float snapNear(float deg) { - float snapped = Math.round(deg / 90f) * 90f; - if (Math.abs(deg - snapped) <= SNAP_THRESHOLD) return snapped; - return deg; - } - - private float snapRotation(float deg) { - return Math.round(deg / 90f) * 90f; - } - - private void moveCursor(float dx, float dy) { - if (fbWidth <= 0 || fbHeight <= 0) return; - double rad = Math.toRadians(ivDisplay.getRotation()); - float cos = (float) Math.cos(rad), sin = (float) Math.sin(rad); - float vncDx = dx * cos + dy * sin; - float vncDy = -dx * sin + dy * cos; - cursorX = max(0, min(cursorX + vncDx, fbWidth - 1)); - cursorY = max(0, min(cursorY + vncDy, fbHeight - 1)); - vncClient.sendPointer((int) cursorX, (int) cursorY, 0); - ensureCursorVisible(); - } - - private void ensureCursorVisible() { - if (fbWidth <= 0 || fbHeight <= 0) return; - int cW = displayContainer.getWidth(); - int cH = displayContainer.getHeight(); - int viewW = currentViewW(); - int viewH = currentViewH(); - if (cW <= 0 || cH <= 0 || viewW <= 0 || viewH <= 0) return; - double rad = Math.toRadians(ivDisplay.getRotation()); - float cos = (float) Math.cos(rad), sin = (float) Math.sin(rad); - float localX = cursorX * viewW / (float) fbWidth; - float localY = cursorY * viewH / (float) fbHeight; - float relX = localX - viewW / 2f; - float relY = localY - viewH / 2f; - float rotX = relX * cos - relY * sin; - float rotY = relX * sin + relY * cos; - float screenX = cW / 2f + panX + rotX; - float screenY = cH / 2f + panY + rotY; - if (screenX < 0) panX -= screenX; - else if (screenX > cW) panX -= (screenX - cW); - if (screenY < 0) panY -= screenY; - else if (screenY > cH) panY -= (screenY - cH); - clampPan(); - ivDisplay.setTranslationX(panX); - ivDisplay.setTranslationY(panY); - updateOperationLabelPosition(); - } - - private void updateOperationLabelPosition() { - if (operationLabel == null) return; - int cW = displayContainer.getWidth(); - int cH = displayContainer.getHeight(); - if (cW <= 0 || cH <= 0) return; - float r = ivDisplay.getRotation(); - operationLabel.setRotation(r); - int deg = ((Math.round(r / 90f) % 4) + 4) % 4; - var lp = (FrameLayout.LayoutParams) operationLabel.getLayoutParams(); - int m = (int) dp(8); - switch (deg) { - case 0: - lp.gravity = TOP | CENTER_HORIZONTAL; - lp.setMargins(0, m, 0, 0); - break; - case 1: - lp.gravity = END | CENTER_VERTICAL; - lp.setMargins(0, 0, m, 0); - break; - case 2: - lp.gravity = BOTTOM | CENTER_HORIZONTAL; - lp.setMargins(0, 0, 0, m); - break; - default: - lp.gravity = START | CENTER_VERTICAL; - lp.setMargins(m, 0, 0, 0); - break; - } - operationLabel.setLayoutParams(lp); - } - private void showOperation(int resId) { if (operationLabel == null) return; operationLabel.setText(resId); @@ -605,56 +631,8 @@ private void showOperation(int resId) { mainHandler.postDelayed(hideOperationLabel, OP_LABEL_HIDE_DELAY_MS); } - private void updateAspectRatio(int containerW, int containerH) { - if (containerW <= 0 || containerH <= 0 || fbWidth <= 0 || fbHeight <= 0) return; - float vmAspect = (float) fbWidth / fbHeight; - float containerAspect = (float) containerW / containerH; - if (vmAspect > containerAspect) { - baseViewW = containerW; - baseViewH = Math.round(containerW / vmAspect); - } else { - baseViewH = containerH; - baseViewW = Math.round(containerH * vmAspect); - } - applyViewSize(); - if (inputMode == InputMode.MOUSE) ensureCursorVisible(); - } - - private void showBars() { - toolbar.setVisibility(VISIBLE); - statusBar.setVisibility(VISIBLE); - if (status == VncStatus.CONNECTED) - mainHandler.postDelayed(this::hideBars, AUTO_HIDE_DELAY_MS); - } - - private void hideBars() { - if (isFullscreen) return; - toolbar.setVisibility(GONE); - statusBar.setVisibility(GONE); - } - private void toggleFullscreen() { - isFullscreen = !isFullscreen; - var controller = getWindow().getInsetsController(); - if (controller == null) return; - if (isFullscreen) { - mainHandler.removeCallbacks(this::hideBars); - toolbar.setVisibility(GONE); - statusBar.setVisibility(GONE); - extraKeysPanel.setVisibility(GONE); - controller.hide(WindowInsets.Type.systemBars()); - controller.setSystemBarsBehavior(BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); - } else { - showBars(); - if (extraKeysVisible) extraKeysPanel.animateIn(); - controller.show(WindowInsets.Type.systemBars()); - } - ViewCompat.requestApplyInsets(findViewById(android.R.id.content)); - } - - private void toggleExtraKeys() { - extraKeysVisible = !extraKeysVisible; - extraKeysPanel.setVisibleAnimated(extraKeysVisible); + chrome.toggleFullscreen(); } @SuppressLint("ClickableViewAccessibility") @@ -666,32 +644,57 @@ private void setupFab() { private void showFabMenu() { var popup = new MaterialMenu(this, fabMenu); popup.inflate(R.menu.menu_vnc_display_menu); - var item = popup.getMenu().findItem(R.id.menu_input_mode); - if (item != null) { - if (inputMode == InputMode.TOUCH) { - item.setTitle(R.string.vnc_menu_input_mode_mouse); - item.setIcon(R.drawable.ic_mouse); - } else { - item.setTitle(R.string.vnc_menu_input_mode_touch); - item.setIcon(R.drawable.ic_touchpad); - } - } + var header = new LinearLayout(this); + header.setOrientation(LinearLayout.VERTICAL); + header.addView(buildInputModeHeader(popup)); + header.addView(DisplayKeyboardMenuRow.build( + getLayoutInflater(), chrome.getKeyboardMode(), this::applyKeyboardMode, + popup::dismiss)); + popup.setHeaderView(header); popup.setOnMenuItemClickListener(this::onMenuItemClicked); popup.show(); } + // Menu header: one row of three icon buttons (touch / tablet / mouse), active mode checked. + private View buildInputModeHeader(MaterialMenu popup) { + var group = (com.google.android.material.button.MaterialButtonToggleGroup) + getLayoutInflater().inflate(R.layout.view_input_mode_toggle, null); + group.check(inputMode == InputMode.MOUSE ? R.id.mode_mouse + : inputMode == InputMode.TABLET ? R.id.mode_tablet : R.id.mode_touch); + group.addOnButtonCheckedListener((g, checkedId, isChecked) -> { + if (!isChecked) return; + setInputMode(checkedId == R.id.mode_mouse ? InputMode.MOUSE + : checkedId == R.id.mode_tablet ? InputMode.TABLET : InputMode.TOUCH); + popup.dismiss(); + }); + return group; + } + + // Selecting the system keyboard summons the IME; anything else puts it away, so the mode + // and what is actually on screen agree. + private void applyKeyboardMode(@NonNull KeyboardMode mode) { + chrome.setKeyboardMode(mode); + if (mode == KeyboardMode.SYSTEM) toggleSoftKeyboard(); + else hideSoftKeyboard(); + } + + // Dropping the display view's focus first matters: it is what the IME is attached to, and + // some ROMs re-show the keyboard for a still-focused target right after a hide request. + private void hideSoftKeyboard() { + ivDisplay.clearFocus(); + var controller = WindowCompat.getInsetsController(getWindow(), ivDisplay); + controller.hide(WindowInsetsCompat.Type.ime()); + var imm = getSystemService(android.view.inputmethod.InputMethodManager.class); + if (imm != null) + imm.hideSoftInputFromWindow(displayContainer.getWindowToken(), 0); + } + @Override protected boolean onMenuItemClicked(@NonNull MenuItem item) { int id = item.getItemId(); - if (id == R.id.menu_extra_keys) { - toggleExtraKeys(); - return true; - } else if (id == R.id.menu_fullscreen) { + if (id == R.id.menu_fullscreen) { toggleFullscreen(); return true; - } else if (id == R.id.menu_input_mode) { - setInputMode(inputMode == InputMode.TOUCH - ? InputMode.MOUSE : InputMode.TOUCH); return true; } return super.onMenuItemClicked(item); diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/display/VncBitmapSource.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/display/VncBitmapSource.java new file mode 100644 index 000000000..d73be2a93 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/display/VncBitmapSource.java @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. +package cn.classfun.droidvm.ui.vm.display.vnc.display; + +import androidx.annotation.NonNull; + +import cn.classfun.droidvm.ui.vm.display.base.DisplaySource; + +/** + * {@link DisplaySource} adapter for the VNC display path. The RFB connection, framebuffer bitmap + * and reconnect logic live in BaseVncActivity (which owns the whole connection lifecycle, so + * {@link #start()}/{@link #shutdown()} are no-ops here); the activity feeds framebuffer events in + * through the dispatch methods. This keeps the VNC console speaking the same source language as + * the native path until the connection plumbing itself moves in here with the base-activity + * unification. No guest-resize channel (would be RFB desktop-size) yet. + */ +public final class VncBitmapSource implements DisplaySource { + private final Callbacks callbacks; + + public VncBitmapSource(@NonNull Callbacks callbacks) { + this.callbacks = callbacks; + } + + /** BaseVncActivity's onFramebufferReady hook lands here. */ + public void dispatchContentSize(int width, int height) { + callbacks.onContentSize(width, height); + } + + /** BaseVncActivity's status hook lands here (currently informational only). */ + public void dispatchState(@NonNull State state) { + callbacks.onStateChanged(state); + } + + @Override + public void start() { + } + + @Override + public void shutdown() { + } + + @Override + public boolean supportsGuestResize() { + return false; + } + + @Override + public void requestGuestResize(int width, int height) { + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/input/VncExtraKeysPanel.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/input/VncExtraKeysPanel.java index 2bc4c3649..f9dbbcf65 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/input/VncExtraKeysPanel.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/input/VncExtraKeysPanel.java @@ -3,7 +3,6 @@ // Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.ui.vm.display.vnc.input; -import static android.view.KeyEvent.KEYCODE_CAPS_LOCK; import static cn.classfun.droidvm.ui.vm.display.base.X11Keymap.androidKeyToXKeysym; import androidx.annotation.NonNull; @@ -15,8 +14,8 @@ /** * Adapts the shared {@link DisplayExtraKeysPanel} to a VNC backend, emitting X keysyms through - * {@link VncClient}. The sticky-modifier handling lives in {@link BaseExtraKeysAdapter}; only the - * emit/ready hooks and the keysym-level send helpers are backend-specific. + * {@link VncClient}. The sticky-modifier and key down/up handling live in + * {@link BaseExtraKeysAdapter}; only the emit/ready hooks are backend-specific. */ public final class VncExtraKeysPanel extends BaseExtraKeysAdapter { @Nullable @@ -38,42 +37,12 @@ public DisplayExtraKeysPanel getPanel() { @Override protected void emitKey(int androidKeyCode, boolean down) { - if (vncClient != null) vncClient.sendKey(androidKeyToXKeysym(androidKeyCode), down); + int keysym = androidKeyToXKeysym(androidKeyCode); + if (keysym != 0 && vncClient != null) vncClient.sendKey(keysym, down); } @Override protected boolean isReady() { return vncClient != null && vncClient.isConnected(); } - - public void sendKeysym(int keysym) { - if (!isReady() || keysym == 0) return; - applyModifiers(true); - vncClient.sendKey(keysym, true); - vncClient.sendKey(keysym, false); - applyModifiers(false); - } - - public void sendKey(int androidKeyCode) { - sendKeysym(androidKeyToXKeysym(androidKeyCode)); - } - - public void sendChar(char ch) { - sendKeysym(ch); - } - - @Override - public void onKeyRepeat(int androidKeyCode) { - sendKey(androidKeyCode); - } - - @Override - public void onCharRepeat(char ch) { - sendChar(ch); - } - - @Override - public void onCapsToggle(boolean active) { - sendKey(KEYCODE_CAPS_LOCK); - } } diff --git a/app/src/main/res/drawable/extra_key_bg.xml b/app/src/main/res/drawable/extra_key_bg.xml new file mode 100644 index 000000000..e89794c07 --- /dev/null +++ b/app/src/main/res/drawable/extra_key_bg.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_key_backspace.xml b/app/src/main/res/drawable/ic_key_backspace.xml new file mode 100644 index 000000000..e1fec3b7d --- /dev/null +++ b/app/src/main/res/drawable/ic_key_backspace.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_key_enter.xml b/app/src/main/res/drawable/ic_key_enter.xml new file mode 100644 index 000000000..11ba80158 --- /dev/null +++ b/app/src/main/res/drawable/ic_key_enter.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_key_keyboard.xml b/app/src/main/res/drawable/ic_key_keyboard.xml new file mode 100644 index 000000000..63ec2dc4e --- /dev/null +++ b/app/src/main/res/drawable/ic_key_keyboard.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_key_shift.xml b/app/src/main/res/drawable/ic_key_shift.xml new file mode 100644 index 000000000..ba38f1bb5 --- /dev/null +++ b/app/src/main/res/drawable/ic_key_shift.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_key_space.xml b/app/src/main/res/drawable/ic_key_space.xml new file mode 100644 index 000000000..5648c2635 --- /dev/null +++ b/app/src/main/res/drawable/ic_key_space.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_key_tab.xml b/app/src/main/res/drawable/ic_key_tab.xml new file mode 100644 index 000000000..756d47ca4 --- /dev/null +++ b/app/src/main/res/drawable/ic_key_tab.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_key_windows.xml b/app/src/main/res/drawable/ic_key_windows.xml new file mode 100644 index 000000000..b6064b368 --- /dev/null +++ b/app/src/main/res/drawable/ic_key_windows.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_keyboard_close.xml b/app/src/main/res/drawable/ic_keyboard_close.xml new file mode 100644 index 000000000..b463b6dab --- /dev/null +++ b/app/src/main/res/drawable/ic_keyboard_close.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_keyboard_phy.xml b/app/src/main/res/drawable/ic_keyboard_phy.xml new file mode 100644 index 000000000..81990c40a --- /dev/null +++ b/app/src/main/res/drawable/ic_keyboard_phy.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_tablet_stylus.xml b/app/src/main/res/drawable/ic_tablet_stylus.xml new file mode 100644 index 000000000..9b685a839 --- /dev/null +++ b/app/src/main/res/drawable/ic_tablet_stylus.xml @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/ic_touchscreen.xml b/app/src/main/res/drawable/ic_touchscreen.xml new file mode 100644 index 000000000..09a8fde68 --- /dev/null +++ b/app/src/main/res/drawable/ic_touchscreen.xml @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/app/src/main/res/layout/activity_vm_native_display.xml b/app/src/main/res/layout/activity_vm_native_display.xml index 46f492380..413d24268 100644 --- a/app/src/main/res/layout/activity_vm_native_display.xml +++ b/app/src/main/res/layout/activity_vm_native_display.xml @@ -6,7 +6,6 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/vnc_background" - android:fitsSystemWindows="true" android:orientation="vertical" tools:context=".ui.vm.display.nativedisplay.display.VMNativeDisplayActivity"> @@ -69,6 +68,30 @@ android:layout_height="match_parent" android:layout_gravity="center" /> + + + @@ -141,4 +164,11 @@ android:gravity="bottom" android:orientation="vertical" /> + + diff --git a/app/src/main/res/layout/activity_vm_vnc_display.xml b/app/src/main/res/layout/activity_vm_vnc_display.xml index 90672de5d..38de37dde 100644 --- a/app/src/main/res/layout/activity_vm_vnc_display.xml +++ b/app/src/main/res/layout/activity_vm_vnc_display.xml @@ -140,4 +140,11 @@ android:gravity="bottom" android:orientation="vertical" /> + + diff --git a/app/src/main/res/layout/view_input_mode_toggle.xml b/app/src/main/res/layout/view_input_mode_toggle.xml new file mode 100644 index 000000000..60ff859b5 --- /dev/null +++ b/app/src/main/res/layout/view_input_mode_toggle.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/view_keyboard_mode_toggle.xml b/app/src/main/res/layout/view_keyboard_mode_toggle.xml new file mode 100644 index 000000000..9b3133b1d --- /dev/null +++ b/app/src/main/res/layout/view_keyboard_mode_toggle.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/widget_display_extra_keys.xml b/app/src/main/res/layout/widget_display_extra_keys.xml index cb3db1159..0aeee3fe9 100644 --- a/app/src/main/res/layout/widget_display_extra_keys.xml +++ b/app/src/main/res/layout/widget_display_extra_keys.xml @@ -1,162 +1,124 @@ + - -
* * The binder can't ride the daemon's TCP/JSON-RPC channel, so it is broadcast to the UI through @@ -63,11 +64,11 @@ public IBinder waitForDisplayBinder(String serviceName) { } @Override - public boolean writeInput(String vmId, int channel, byte[] data) { + public boolean writeInput(String vmId, String screenId, int channel, byte[] data) { if (vmId == null || data == null || data.length == 0) return false; var inst = ctx.getVMs().findById(vmId); if (inst == null) return false; - return inst.writeNativeInput(channel, data); + return inst.writeNativeInput(screenId == null ? "" : screenId, channel, data); } }; } @@ -136,6 +137,12 @@ private static IBinder pollForService(@NonNull String name) { * so they wait as before. */ private static boolean vmCouldRegister(@NonNull ServerContext ctx, @NonNull String serviceName) { + // NativeDisplay owns both halves of the name -- the VM's channel root and the screen id + // appended to it -- so the reverse mapping lives there too rather than as a prefix strip + // written out again here, which is how it would silently stop matching. + var vmId = NativeDisplay.vmIdFromServiceName(serviceName); + if (vmId.isEmpty()) return true; + var inst = ctx.getVMs().findById(vmId); if (inst == null) return true; var state = inst.getState(); return state == VMState.RUNNING || state == VMState.STARTING || state == VMState.REBOOTING; diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/ListHostAddressesHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/ListHostAddressesHandler.java new file mode 100644 index 000000000..b056e965e --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/network/ListHostAddressesHandler.java @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. +package cn.classfun.droidvm.daemon.ipc.network; + +import androidx.annotation.NonNull; + +import com.google.auto.service.AutoService; + +import cn.classfun.droidvm.daemon.network.backend.HostAddressScan; +import cn.classfun.droidvm.daemon.server.ClientRequest; +import cn.classfun.droidvm.daemon.server.RequestHandler; + +/** + * The phone's own addresses, for a UI offering somewhere to listen. + * + *

Asked of the daemon rather than enumerated in the app, because the two addresses that must + * not appear -- a pseudo-bridged guest's IP parked on the uplink, and the host-route-only shape + * pbridge parks it in -- are netlink details no unprivileged interface enumeration can see. See + * {@link HostAddressScan#list()} for the whole policy.

+ */ +@AutoService(RequestHandler.class) +public final class ListHostAddressesHandler extends RequestHandler { + @NonNull + @Override + public String getName() { + return "network_list_host_addresses"; + } + + @Override + public void handle(@NonNull ClientRequest request) throws Exception { + request.res().put("data", HostAddressScan.list()); + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/InputHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/InputHandler.java index 0bcc6c209..04e692143 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/InputHandler.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/InputHandler.java @@ -19,7 +19,9 @@ * Forwards native-display input from the UI to the per-VM crosvm process. The daemon is the only * listener on crosvm's --input sockets (it pre-binds them before exec'ing crosvm), so it is the * only process that can deliver evdev to the guest; the UI sends bytes here instead of writing a - * socket directly. Params: vm_id, channel (NativeDisplay constants), data (base64 evdev records). + * socket directly. Params: vm_id, screen (the screen the sending console shows, which picks + * between two screens' absolute devices; ignored by the VM-wide keyboard and relative pointer), + * channel (NativeDisplay constants), data (base64 evdev records). */ @AutoService(RequestHandler.class) public final class InputHandler extends RequestHandler { @@ -35,13 +37,15 @@ public void handle(@NonNull ClientRequest request) throws Exception { var vmId = params.optString("vm_id", ""); if (vmId.isEmpty()) throw new RequestException("missing vm_id"); + var screenId = params.optString("screen", ""); var channel = params.optInt("channel", -1); var data = Base64.decode(params.optString("data", ""), Base64.NO_WRAP); var inst = request.getContext().getVMs().findById(vmId); if (inst == null) throw new RequestException(fmt("VM not found: %s", vmId)); // Report whether the bytes actually reached crosvm so the UI can tell a silent drop (peer - // not connected yet, bad channel, VM not running) from a real delivery. - request.res().put("delivered", inst.writeNativeInput(channel, data)); + // not connected yet, bad channel, no absolute device on that screen, VM not running) from + // a real delivery. + request.res().put("delivered", inst.writeNativeInput(screenId, channel, data)); } } diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/VncInfoHandler.java b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/VncInfoHandler.java index 21329dd48..c3212cff2 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/VncInfoHandler.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/ipc/vm/VncInfoHandler.java @@ -10,6 +10,8 @@ import cn.classfun.droidvm.daemon.server.ClientRequest; import cn.classfun.droidvm.daemon.server.RequestException; import cn.classfun.droidvm.daemon.server.RequestHandler; +import cn.classfun.droidvm.lib.store.vm.DisplayExporter; +import cn.classfun.droidvm.lib.store.vm.VMScreenConfig; @AutoService(RequestHandler.class) public final class VncInfoHandler extends RequestHandler { @@ -28,13 +30,25 @@ public void handle(@NonNull ClientRequest request) throws Exception { var inst = request.getContext().getVMs().findById(vmId); if (inst == null) throw new RequestException("VM not found"); - if (!inst.item.optBoolean("vnc_enabled", false)) + // Which screen's server. A client that names none gets the first VNC-bound screen, which + // is the only one a single-screen VM has and the one its default view opens. + var screenId = params.optString("screen", ""); + VMScreenConfig screen = null; + for (var candidate : VMScreenConfig.listOf(inst.item)) { + if (!candidate.isEnabled() || candidate.getExporter() != DisplayExporter.VNC) continue; + if (screenId.isEmpty() || screenId.equals(candidate.id)) { + screen = candidate; + break; + } + } + if (screen == null) throw new RequestException("VNC is not enabled for this VM"); var res = request.res(); - var host = inst.item.optString("vnc_host", ""); + var host = screen.getVncHost(); + res.put("screen", screen.id); res.put("host", !host.isEmpty() ? host : "127.0.0.1"); - res.put("port", inst.item.optLong("vnc_port", -1)); - res.put("password", inst.item.optString("vnc_password", "")); + res.put("port", screen.getVncPort()); + res.put("password", screen.getVncPassword()); // When VNC binds to the IPv4 wildcard, resolve the phone's own LAN // address here from the router watcher's filtered host-IP set, which // already drops pbridge offload-proxy addresses parked on the uplink. diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/DefaultRouterWatcher.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/DefaultRouterWatcher.java index 4f5a86474..38a2f5e21 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/DefaultRouterWatcher.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/DefaultRouterWatcher.java @@ -43,17 +43,14 @@ public final class DefaultRouterWatcher { */ private static final int RULE_PRIORITY_BASE = 9000; /** - * Interface-name prefixes whose IPv4 addresses count as the phone's own - * reachable IPs for port-forward DNAT scoping: Wi-Fi, cellular, VPN, - * ethernet, and hotspot/USB/BT tethering. Passed to netbox, which also - * drops bridge devices and pbridge-offload addresses. Cellular names other - * than rmnet_data (ccmni, pdp_ip...) are intentionally not matched -- same - * assumption the iptables EXT_IFACES list already makes. + * The prefixes whose addresses count as the phone's own, for port-forward + * DNAT scoping. Passed to netbox, which also drops bridge devices and + * pbridge-offload addresses. Shared with {@link HostAddressScan}, which + * answers the same question for the VNC listen-address picker and applies + * the rest of the same policy in Java; the list lives there. */ - private static final List HOST_IFACE_PREFIXES = List.of( - "wlan", "rmnet_data", "tun", "eth", - "ap", "swlan", "softap", "rndis", "usb", "bt-pan" - ); + private static final List HOST_IFACE_PREFIXES = + HostAddressScan.HOST_IFACE_PREFIXES; private final ServerContext context; private final List listeners = new CopyOnWriteArrayList<>(); private final List hostIpListeners = new CopyOnWriteArrayList<>(); diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/HostAddressScan.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/HostAddressScan.java new file mode 100644 index 000000000..a1964e9c4 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/HostAddressScan.java @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. +package cn.classfun.droidvm.daemon.network.backend; + +import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; + +import android.util.Log; + +import androidx.annotation.NonNull; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import cn.classfun.droidvm.lib.Constants; + +/** + * The phone's own addresses, as a list something can be asked to listen on. + * + *

Both families, unlike {@link Netbox#hostIpv4}, and one shot rather than a watched set: this + * answers a picker that is open for a few seconds, not a firewall rule that has to follow the + * network. The policy is otherwise the same one, and lives here because the daemon is the only + * side that can apply it -- see the two exclusions below, neither of which + * {@code java.net.NetworkInterface} can see.

+ */ +public final class HostAddressScan { + private static final String TAG = "HostAddressScan"; + + /** + * Interface-name prefixes whose addresses count as the phone's own reachable IPs: Wi-Fi, + * cellular, VPN, ethernet, and hotspot/USB/BT tethering. Cellular names other than + * {@code rmnet_data} (ccmni, pdp_ip...) are intentionally not matched -- the same assumption + * the iptables EXT_IFACES list already makes. + * + *

One list, two readers: the port-forward DNAT scoping this was written for (through + * netbox's own {@code host-ips} policy) and the VNC listen-address picker. They are asking the + * same question, so they must not answer it from two lists.

+ */ + public static final List HOST_IFACE_PREFIXES = List.of( + "wlan", "rmnet_data", "tun", "eth", + "ap", "swlan", "softap", "rndis", "usb", "bt-pan" + ); + + private HostAddressScan() { + } + + /** + * Every address the phone itself holds, as {@code [{addr, ifname, family}]} -- v4 and v6, + * global scope only, in netlink's dump order. Empty on any failure, which the caller shows as + * "nothing was found" rather than as an error: the picker's two fixed entries and its custom + * dialog work without this list. + * + *

Four things are dropped, and the first two are the whole reason this is not done in the + * app process:

+ * + *
    + *
  • pbridge's offload-proxy addresses. An L2 pseudo-bridged guest's IP is parked on the + * phone's uplink so the Wi-Fi firmware answers ARP/NS for it, which makes it look exactly + * like an address of the phone's own. It is tagged with {@link Constants#PBRIDGE_OFFLOAD_MAGIC} + * as IFA_RT_PRIORITY for precisely this -- and that tag is netlink-only, invisible to + * {@code java.net.NetworkInterface} and to OEM {@code ip -j} builds too old to emit it.
  • + *
  • a host-route-only address ({@code noprefixroute} on a /32 or /128), which is the shape + * pbridge parks, so a proxy address from a build that predates the tag is caught anyway. Both + * halves are required: a plain /32 is how plenty of cellular interfaces are configured, and + * dropping those would lose the phone's real address.
  • + *
  • IPv6 privacy addresses ({@code temporary}). There are normally several at once -- + * the live one plus however many are still inside their valid lifetime and deprecated -- and + * every one of them is replaced within hours, so naming one in a config is naming something + * that will be gone. The address kept is the stable one the phone keeps alongside them. The + * flag is IFA_F_SECONDARY, which on v4 means an ordinary secondary address and is not + * dropped; netbox reports the v6 reading only.
  • + *
  • bridge devices -- ours, holding a VM network's gateway address rather than the + * phone's.
  • + *
  • anything outside {@link #HOST_IFACE_PREFIXES}, and any address that is not global + * scope: that is where link-local (fe80::/10, 169.254/16) and loopback go, neither of which + * is an address the phone can be reached at.
  • + *
+ */ + @NonNull + public static JSONArray list() { + var out = new JSONArray(); + var bridges = bridgeNames(); + var rows = Netbox.addrList(null, null); + for (int i = 0; i < rows.length(); i++) { + var r = rows.optJSONObject(i); + if (r == null) continue; + var addr = r.optString("local", ""); + var ifname = r.optString("ifname", ""); + if (addr.isEmpty() || ifname.isEmpty()) continue; + if (!"global".equals(r.optString("scope", ""))) continue; + if (bridges.contains(ifname)) continue; + if (!isHostIface(ifname)) continue; + if (r.optLong("metric", 0) == Constants.PBRIDGE_OFFLOAD_MAGIC) continue; + var family = r.optInt("family", 0); + if (family != 4 && family != 6) continue; + if (r.optBoolean("temporary", false)) continue; + var prefixlen = r.optInt("prefixlen", 0); + if (r.optBoolean("noprefixroute", false) + && prefixlen == (family == 4 ? 32 : 128)) continue; + try { + var entry = new JSONObject(); + entry.put("addr", addr); + entry.put("ifname", ifname); + entry.put("family", family); + out.put(entry); + } catch (Exception e) { + Log.w(TAG, fmt("Failed to build host address entry for %s", addr), e); + } + } + return out; + } + + private static boolean isHostIface(@NonNull String ifname) { + for (var prefix : HOST_IFACE_PREFIXES) + if (ifname.startsWith(prefix)) return true; + return false; + } + + @NonNull + private static Set bridgeNames() { + var names = new HashSet(); + var rows = Netbox.linkList(null, true); + for (int i = 0; i < rows.length(); i++) { + var r = rows.optJSONObject(i); + if (r == null) continue; + var name = r.optString("ifname", ""); + if (!name.isEmpty()) names.add(name); + } + return names; + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMBackendInstance.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMBackendInstance.java index 879bd282e..6cb90ae30 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMBackendInstance.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMBackendInstance.java @@ -51,9 +51,11 @@ protected VMBackendInstance(@NonNull ServerContext context, @NonNull VMConfig co /** * Writes pre-encoded evdev bytes to the running backend's native-display input channel on - * behalf of the UI. Only the crosvm backend implements this; others report not-delivered. + * behalf of the UI. [screenId] is the screen the console sending them is showing; it selects + * the device for the absolute channels and is ignored by the VM-wide ones. Only the crosvm + * backend implements this; others report not-delivered. */ - public boolean writeNativeInput(int channel, @NonNull byte[] data) { + public boolean writeNativeInput(@NonNull String screenId, int channel, @NonNull byte[] data) { return false; } diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMInstance.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMInstance.java index 69c5c6a83..44bdfa1e6 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMInstance.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/VMInstance.java @@ -34,6 +34,8 @@ import cn.classfun.droidvm.lib.natives.UnixHelper; import cn.classfun.droidvm.lib.store.base.DataItem; import cn.classfun.droidvm.lib.store.network.NetworkState; +import cn.classfun.droidvm.lib.store.vm.DisplayExporter; +import cn.classfun.droidvm.lib.store.vm.VMScreenConfig; import cn.classfun.droidvm.lib.store.vm.VMBackend; import cn.classfun.droidvm.lib.store.vm.VMConfig; import cn.classfun.droidvm.lib.store.vm.VMNicConfig; @@ -98,12 +100,16 @@ synchronized VMBackendInstance getBackendInstance() { return this.backendInstance; } - /** Forwards UI-sent evdev bytes to the running backend's native-display input channel. */ - public boolean writeNativeInput(int channel, @NonNull byte[] data) { + /** + * Forwards UI-sent evdev bytes to the running backend's native-display input channel for + * [screenId] -- the screen the console that sent them is showing, which is what picks between + * two screens' absolute devices. + */ + public boolean writeNativeInput(@NonNull String screenId, int channel, @NonNull byte[] data) { // Only a running VM has a backend with bound input sockets; skip otherwise so a stale or // spoofed input call can't lazily create an idle backend instance. if (state != VMState.RUNNING) return false; - return getBackendInstance().writeNativeInput(channel, data); + return getBackendInstance().writeNativeInput(screenId, channel, data); } @NonNull @@ -175,7 +181,7 @@ public boolean start() { } joinThreads(1000); if (!setupTaps()) return false; - if (item.optBoolean("vnc_enabled", false)) resolveVncConfig(); + resolveVncConfig(); stoppedByUser = false; exitCode = -1; setState(VMState.STARTING); @@ -261,20 +267,39 @@ private void detachNic(@NonNull VMNicConfig nic) { } } + /** + * Fills in whatever each VNC-bound screen left unset before the VM starts. + * + *

Per screen, not per VM: two screens exporting over VNC are two servers, and crosvm + * refuses to start when they land on the same port -- so an unset port is resolved once for + * each of them, and the one just handed out is held against the next lookup because it is not + * bound yet and would otherwise still look free.

+ */ private void resolveVncConfig() { - if (item.optLong("vnc_port", -1) <= 0) { - int port = generateRandomAvailablePort(); - if (port > 0) { - item.set("vnc_port", port); - Log.i(TAG, fmt("VM %s: auto-assigned VNC port %d", getName(), port)); - } else { - Log.e(TAG, fmt("VM %s: failed to find available VNC port", getName())); + var taken = new ArrayList(); + for (var screen : VMScreenConfig.listOf(item)) { + if (!screen.isEnabled() || screen.getExporter() != DisplayExporter.VNC) continue; + var port = screen.getVncPort(); + if (port <= 0 || taken.contains(port)) { + int fresh = generateRandomAvailablePort(); + while (fresh > 0 && taken.contains((long) fresh)) + fresh = generateRandomAvailablePort(); + if (fresh > 0) { + screen.setVncPort(fresh); + port = fresh; + Log.i(TAG, fmt("VM %s: auto-assigned VNC port %d for screen %s", + getName(), fresh, screen.id)); + } else { + Log.e(TAG, fmt("VM %s: failed to find available VNC port for screen %s", + getName(), screen.id)); + } + } + if (port > 0) taken.add(port); + if (screen.isVncPasswordAuth() && screen.getVncPassword().isEmpty()) { + screen.setVncPassword(generateRandomPassword(8)); + Log.i(TAG, fmt("VM %s: auto-generated VNC password for screen %s", + getName(), screen.id)); } - } - if (item.optBoolean("vnc_password_auth", false) && item.optString("vnc_password", "").isEmpty()) { - var password = generateRandomPassword(8); - item.set("vnc_password", password); - Log.i(TAG, fmt("VM %s: auto-generated VNC password", getName())); } } diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/NativeDisplayInputBridge.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/NativeDisplayInputBridge.java index a6707065c..015a16a34 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/NativeDisplayInputBridge.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/NativeDisplayInputBridge.java @@ -9,91 +9,172 @@ import android.util.Log; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; import cn.classfun.droidvm.lib.natives.UnixHelper; import cn.classfun.droidvm.lib.network.FDSocket; import cn.classfun.droidvm.lib.store.vm.NativeDisplay; /** - * Owns the per-VM native-display input sockets for one crosvm instance. crosvm's --input + * Owns the native-display input sockets for one crosvm instance. crosvm's --input * [path=...] connects to a unix socket whose inode must already exist (crosvm is the * *client*), so the daemon is the only process that can both bind the socket before crosvm starts * and stay alive to feed it. This bridge pre-binds + accepts the crosvm-facing sockets; evdev from - * the UI arrives via {@link #writeNativeInput(int, byte[])} - called either on the daemon's + * the UI arrives via {@link #writeNativeInput(String, int, byte[])} - called either on the daemon's * native-display broker binder thread (touch hot path) or from the vm_input IPC handler - and is * written straight to the matching crosvm peer. {@link CrosvmBackendInstance} drives the lifecycle: - * {@link #startListening(String)} from start() and {@link #release()} from cleanup(). + * {@link #startListening(String, List, List)} from start() and {@link #release()} from cleanup(). + * + *

The socket set is not one per channel: the relative pointer is VM-wide, while multi-touch, + * the absolute pointer and the keyboard exist once per screen that has them, so a slot is a + * (screen, channel) pair and the screens that get one are decided by the config. Everything here + * is therefore keyed rather than indexed -- a flat channel-indexed array cannot say which screen a + * write is for, and silently picking one would put touches, or typing, on the wrong output.

+ * + *

The three per-screen channels do not cover the same screens, which is why they arrive as two + * lists rather than one. A VNC-exported screen's absolute pointer and keyboard are not ours: + * crosvm builds that screen's pair behind its own VNC server and feeds them from RFB pointer and + * key events, so there is no {@code --input} for either and a socket bound here would be an inode + * crosvm never connects to. Its touchscreen is still ours on the same terms as a native screen's, + * because multi-touch has no RFB event to arrive as.

*/ final class NativeDisplayInputBridge { private static final String TAG = "NativeDisplayInput"; - /** Server fds for the per-VM native-display input sockets, kept while crosvm is running. */ - private volatile int[] inputServerFds = null; - /** Paths of the input sockets we listened on, so {@link #release()} can unlink them. */ - private volatile String[] inputSocketPaths = null; /** - * The crosvm-side connection accepted on each input channel. crosvm connects to OUR socket at - * startup (we are the only listener), so writing UI-forwarded evdev here is what actually - * reaches the guest. Indexed by NativeDisplay channel constants. Volatile because the accept - * threads, the write threads, and release() all touch it without a single shared lock. + * One crosvm-facing input socket: the inode we bound, the accepted crosvm connection, and the + * lock that keeps a write and a reconnect off each other. crosvm connects to OUR socket at + * startup (we are the only listener), so writing UI-forwarded evdev to {@link #peer} is what + * actually reaches the guest. */ - private volatile FDSocket[] inputPeers = null; - /** Per-channel write lock; also guards swapping {@link #inputPeers} on reconnect. */ - private volatile Object[] inputWriteLocks = null; + private static final class Slot { + final String key; + final String path; + final int serverFd; + final Object lock = new Object(); + /** Volatile: accept threads, write threads and release() all touch it without one lock. */ + volatile FDSocket peer; + + Slot(@NonNull String key, @NonNull String path, int serverFd) { + this.key = key; + this.path = path; + this.serverFd = serverFd; + } + } + + /** + * Live slots by {@link #slotKey}. Replaced wholesale on start and cleared on release, so a + * reader either sees the whole set or none of it. Null until the first start. + */ + private volatile Map slots = null; private volatile boolean inputClosed = false; /** - * Pre-creates the per-VM native-display input sockets as listening unix sockets. crosvm - * connects to these paths at startup, so a listener must exist before - * {@link CrosvmBackendInstance#start()} execs the crosvm process. nativeUnixListen unlinks any - * stale inode and re-binds, so a leftover socket file from a crashed run is replaced rather than - * blocking us. Returns true iff every channel ended up with a live listener. Server fds we open - * are tracked for release in {@link #release()}. + * Identity of one socket: the channel, plus the screen for the channels that have one per + * screen. The VM-wide channel collapses onto the empty screen so the same key comes out + * whatever screen the console that sent the bytes happens to be showing. + * + *

{@link NativeDisplay#isPerScreen} is the only place that split is decided, so a channel + * becoming per screen moves the socket name and this key together -- there is no second copy + * of the rule here to forget to update.

*/ - boolean startListening(@NonNull String serviceName) { + @NonNull + private static String slotKey(@NonNull String screenId, int channel) { + return fmt("%s/%d", NativeDisplay.isPerScreen(channel) ? screenId : "", channel); + } + + /** + * Pre-creates the input sockets as listening unix sockets. crosvm connects to these paths at + * startup, so a listener must exist before {@link CrosvmBackendInstance#start()} execs the + * crosvm process. nativeUnixListen unlinks any stale inode and re-binds, so a leftover socket + * file from a crashed run is replaced rather than blocking us. Returns true iff every slot + * ended up with a live listener. + * + *

[touchScreens] are the screens that get a multi-touch device and [nativeScreens] the ones + * that get an absolute pointer and a keyboard -- the same two lists + * {@link CrosvmBackendInstance} emits {@code --input} devices from, so the sockets and the + * devices cannot diverge. They are not the same list: the second holds only the natively + * exported screens, because a VNC-exported screen's tablet and keyboard are crosvm's own. A + * screen left out of a list has no socket and no device on that channel; input aimed at it is + * refused rather than landing on some other screen's geometry, or on a screen whose user + * switched input off.

+ * + *

Throws IllegalArgumentException if a path does not fit a unix socket address. That is the + * one failure here that is not survivable and not diagnosable after the fact: bind(2) truncates + * silently, so the daemon would report a live listener on an inode crosvm was never told about. + * The caller turns it into a refused start; see {@link NativeDisplay#requireBindablePath}.

+ */ + boolean startListening(@NonNull String vmId, @NonNull List touchScreens, + @NonNull List nativeScreens) { if (!UnixHelper.isLoaded()) { Log.w(TAG, "UnixHelper not loaded; cannot pre-bind native-display input sockets"); return false; } - var paths = new String[NativeDisplay.CHANNEL_COUNT]; - var fds = new int[NativeDisplay.CHANNEL_COUNT]; inputClosed = false; - inputPeers = new FDSocket[NativeDisplay.CHANNEL_COUNT]; - inputWriteLocks = new Object[NativeDisplay.CHANNEL_COUNT]; + var built = new LinkedHashMap(); boolean allListening = true; for (int ch = 0; ch < NativeDisplay.CHANNEL_COUNT; ch++) { - inputWriteLocks[ch] = new Object(); - var path = NativeDisplay.inputSocketPath(serviceName, ch); - paths[ch] = path; - var fd = UnixHelper.nativeUnixListen(path); - if (fd < 0) { - Log.w(TAG, fmt("Failed to pre-listen on input socket: %s", path)); - allListening = false; - fds[ch] = -1; - } else { + for (var screenId : screensFor(ch, touchScreens, nativeScreens)) { + // Before the syscall, not after: a path bind(2) cannot hold is truncated in + // silence and every check downstream then passes against the wrong inode. + var path = NativeDisplay.requireBindablePath( + NativeDisplay.inputSocketPath(vmId, screenId, ch)); + var fd = UnixHelper.nativeUnixListen(path); + if (fd < 0) { + Log.w(TAG, fmt("Failed to pre-listen on input socket: %s", path)); + allListening = false; + continue; + } Log.i(TAG, fmt("Pre-listening on input socket: %s (fd=%d)", path, fd)); - fds[ch] = fd; + var slot = new Slot(slotKey(screenId, ch), path, fd); + built.put(slot.key, slot); // Accept crosvm's connection in the background. crosvm is the client and connects // at its own startup, so a peer may not arrive until after start() execs it. - startInputAcceptThread(ch, fd); + startInputAcceptThread(slot); } } - inputSocketPaths = paths; - inputServerFds = fds; + slots = built; return allListening; } /** - * Accepts crosvm's connection on one input channel and keeps the live peer in - * {@link #inputPeers}. Loops so a crosvm restart (new connection on the same socket) replaces - * the dead peer; ends when {@link #release()} closes the server fd. + * The screens [channel] needs a socket for: its own list for the three per-screen channels, or + * just the VM itself for the relative pointer, which has no output binding. + * + *

Switched on the channel rather than on {@link NativeDisplay#isPerScreen} alone, because + * "is this per screen" and "which screens" stopped having one answer when the VNC bindings + * took over their own tablets and keyboards. The tablet and the keyboard share a list: both + * are the screen's, both exist only where its input switch is on, and both are crosvm's on a + * VNC binding.

*/ - private void startInputAcceptThread(int channel, int serverFd) { + @NonNull + private static List screensFor(int channel, @NonNull List touchScreens, + @NonNull List nativeScreens) { + switch (channel) { + case NativeDisplay.MULTITOUCH: + return touchScreens; + case NativeDisplay.TABLET: + case NativeDisplay.KEYBOARD: + return nativeScreens; + default: + return List.of(""); + } + } + + /** + * Accepts crosvm's connection on one slot and keeps the live peer on it. Loops so a crosvm + * restart (new connection on the same socket) replaces the dead peer; ends when + * {@link #release()} closes the server fd. + */ + private void startInputAcceptThread(@NonNull Slot slot) { var t = new Thread(() -> { while (!inputClosed) { - int peerFd = UnixHelper.nativeUnixAccept(serverFd); + int peerFd = UnixHelper.nativeUnixAccept(slot.serverFd); if (peerFd < 0) { if (inputClosed) break; try { @@ -104,46 +185,36 @@ private void startInputAcceptThread(int channel, int serverFd) { continue; } var peer = new FDSocket(peerFd); - // Snapshot the lock/peer arrays: release() can null them concurrently. - var locks = inputWriteLocks; - if (locks == null) { - peer.close(); - break; - } - synchronized (locks[channel]) { - var peers = inputPeers; - if (peers == null) { + synchronized (slot.lock) { + if (inputClosed) { peer.close(); break; } - var old = peers[channel]; - peers[channel] = peer; + var old = slot.peer; + slot.peer = peer; if (old != null) old.close(); } - Log.i(TAG, fmt("crosvm input connected: channel %d", channel)); + Log.i(TAG, fmt("crosvm input connected: %s", slot.path)); } - }, fmt("CrosvmInputAccept-%d", channel)); + }, fmt("CrosvmInputAccept-%s", slot.key)); t.setDaemon(true); t.start(); } /** - * Writes pre-encoded evdev bytes (8-byte records) to the crosvm connection for [channel]. - * Called from the daemon IPC thread on behalf of the UI. Returns false if no crosvm peer is - * connected yet or the write fails. + * Writes pre-encoded evdev bytes (8-byte records) to the crosvm connection for [channel] on + * [screenId]. Called from the daemon IPC thread or the broker binder thread on behalf of the + * UI. Returns false if the VM has no such device (a per-screen channel -- touch, tablet or + * keyboard -- on a screen whose input is switched off, or on a VNC-exported screen whose + * tablet and keyboard are crosvm's), no crosvm peer is connected yet, or the write fails -- + * the caller reports that as "not delivered" rather than pretending it landed somewhere. */ - boolean writeNativeInput(int channel, @NonNull byte[] data) { + boolean writeNativeInput(@NonNull String screenId, int channel, @NonNull byte[] data) { if (channel < 0 || channel >= NativeDisplay.CHANNEL_COUNT || data.length == 0) return false; - // Snapshot the arrays once: release() nulls these fields concurrently, so dereferencing the - // live field after the guard could NPE. The lock object itself stays valid for the session. - var locks = inputWriteLocks; - if (locks == null) return false; - var lock = locks[channel]; - if (lock == null) return false; - synchronized (lock) { - var peers = inputPeers; - if (peers == null) return false; - var peer = peers[channel]; + var slot = findSlot(screenId, channel); + if (slot == null) return false; + synchronized (slot.lock) { + var peer = slot.peer; if (peer == null || !peer.isOpen()) return false; try { var os = peer.getOutputStream(); @@ -151,47 +222,36 @@ boolean writeNativeInput(int channel, @NonNull byte[] data) { os.flush(); return true; } catch (IOException e) { - Log.w(TAG, fmt("input write channel %d failed: %s", channel, e.getMessage())); - peers[channel] = null; + Log.w(TAG, fmt("input write to %s failed: %s", slot.path, e.getMessage())); + slot.peer = null; peer.close(); return false; } } } - /** - * Closes the input server fds we opened and unlinks only the inodes we own. Channels that - * fell through to the UI's listener (fd == -1) are left untouched so we don't yank a socket - * the UI still holds. - */ + @Nullable + private Slot findSlot(@NonNull String screenId, int channel) { + // Snapshot the map once: release() nulls the field concurrently. + var live = slots; + return live == null ? null : live.get(slotKey(screenId, channel)); + } + + /** Closes the input server fds we opened and unlinks the inodes we own. */ void release() { inputClosed = true; // stop accept loops; closing the server fd below unblocks nativeUnixAccept - if (inputPeers != null) { - for (int ch = 0; ch < inputPeers.length; ch++) { - if (inputWriteLocks != null && inputWriteLocks[ch] != null) { - synchronized (inputWriteLocks[ch]) { - if (inputPeers[ch] != null) { - inputPeers[ch].close(); - inputPeers[ch] = null; - } - } + var live = slots; + slots = null; + if (live == null) return; + for (var slot : live.values()) { + synchronized (slot.lock) { + if (slot.peer != null) { + slot.peer.close(); + slot.peer = null; } } - inputPeers = null; - } - inputWriteLocks = null; - if (inputServerFds == null) { - inputSocketPaths = null; - return; - } - for (int ch = 0; ch < inputServerFds.length; ch++) { - int fd = inputServerFds[ch]; - if (fd < 0) continue; - UnixHelper.nativeCloseFd(fd); - if (inputSocketPaths != null && inputSocketPaths[ch] != null) - deleteFile(inputSocketPaths[ch]); + UnixHelper.nativeCloseFd(slot.serverFd); + deleteFile(slot.path); } - inputServerFds = null; - inputSocketPaths = null; } } diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/QemuBackendInstance.java b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/QemuBackendInstance.java index 0fef5f5ea..80efe55ef 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/QemuBackendInstance.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/vm/backend/QemuBackendInstance.java @@ -555,6 +555,10 @@ private void buildVncCommand(@NonNull List args) { long displayNum = port - 5900; if (displayNum < 0) displayNum = 0; var vncArg = new StringBuilder(); + // "host:display", so an IPv6 literal has to be bracketed or its own colons are read as the + // separator. Reachable now that the host field offers the phone's own addresses and half of + // those are v6; an IPv4 address is untouched. + if (host.indexOf(':') >= 0) host = fmt("[%s]", host); vncArg.append(fmt("%s:%d", host, displayNum)); var password = bound.getVncPassword(); if (!password.isEmpty()) { diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/enums/EnumPicker.java b/app/src/main/java/cn/classfun/droidvm/lib/store/enums/EnumPicker.java index e8cd851db..79bf89a75 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/enums/EnumPicker.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/enums/EnumPicker.java @@ -16,12 +16,18 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder; import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; public final class EnumPicker> { private final Context context; private final Class enumClass; private final List items = new ArrayList<>(); + /** Items listed but refused; see {@link #setDisabledItems}. */ + private final Set disabled = new LinkedHashSet<>(); + private CharSequence disabledNote = null; private EnumPickerChanged onValueChanged = null; private int selectedIndex = -1; @@ -41,19 +47,15 @@ public AlertDialog showDialog(@Nullable CharSequence title) { if (items.isEmpty()) throw new IllegalStateException("Items cannot be empty"); var labels = new String[items.size()]; - for (int i = 0; i < items.size(); i++) { - var item = items.get(i); - var label = item.toString(); - if (item instanceof StringEnum) { - var se = (StringEnum) item; - label = se.getDisplayString(context); - } - labels[i] = label; - } + for (int i = 0; i < items.size(); i++) + labels[i] = menuLabel(items.get(i)); var b = new MaterialAlertDialogBuilder(context); if (title != null) b.setTitle(title); b.setSingleChoiceItems(labels, selectedIndex, (dialog, which) -> { + // A refused row leaves the dialog open rather than closing on a value it did not + // apply, which would read as having been accepted. + if (disabled.contains(items.get(which))) return; setSelectedIndex(which); dialog.dismiss(); }); @@ -67,12 +69,9 @@ public void showPopup(@NonNull View anchor) { var menu = popup.getMenu(); for (int i = 0; i < items.size(); i++) { var item = items.get(i); - var label = item.name(); - if (item instanceof StringEnum) { - var se = (StringEnum) item; - label = se.getDisplayString(context); - } - menu.add(0, i, i, label); + // MaterialMenu's adapter already honours isEnabled(): it greys the row and swallows + // the tap, so nothing here has to re-check on the way back out. + menu.add(0, i, i, menuLabel(item)).setEnabled(!disabled.contains(item)); } popup.setOnMenuItemClickListener(menuItem -> { setSelectedIndex(menuItem.getItemId()); @@ -81,8 +80,37 @@ public void showPopup(@NonNull View anchor) { popup.show(); } + /** This item's label, plus the disabled note when it is one of the refused ones. */ + @NonNull + private String menuLabel(@NonNull E item) { + var label = item instanceof StringEnum + ? ((StringEnum) item).getDisplayString(context) : item.name(); + if (disabledNote == null || !disabled.contains(item)) return label; + return fmt("%s (%s)", label, disabledNote); + } + + /** + * Items the picker lists but will not select. + * + *

For a set of choices that is easier to read whole than pruned -- a ladder whose upper + * rungs are designed but not built, say. Hiding them makes the remaining values look like the + * entire vocabulary and makes each one that lands later look like a feature out of nowhere; + * listing them greyed, with {@code note} saying why, says what the set is and where this build + * stands in it. A disabled item can still be the current value: that is how a choice stored + * before its rung was refused survives being looked at.

+ * + *

Cleared by {@link #setItems} and {@link #autoItems}, since a new item set has its own + * answer -- the same constant can be reachable under one and not under another.

+ */ + public void setDisabledItems(@Nullable CharSequence note, @NonNull Collection refused) { + disabled.clear(); + disabled.addAll(refused); + disabledNote = note; + } + public void autoItems() { items.clear(); + disabled.clear(); for (var item : getConstants()) { if (item instanceof StringEnum) { var se = (StringEnum) item; @@ -109,6 +137,7 @@ public void setItems(@NonNull List constants) { throw new IllegalArgumentException("Constants cannot be empty"); items.clear(); items.addAll(constants); + disabled.clear(); selectedIndex = -1; setSelectedIndex(0); } diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/DisplayBackend.java b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/DisplayBackend.java index 6e1ed58e5..4735a92e3 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/DisplayBackend.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/DisplayBackend.java @@ -3,44 +3,16 @@ // Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. package cn.classfun.droidvm.lib.store.vm; -import androidx.annotation.StringRes; - -import cn.classfun.droidvm.R; -import cn.classfun.droidvm.lib.store.enums.StringEnum; - -public enum DisplayBackend implements StringEnum { - NONE(0, "none", R.string.nullptr), - SIMPLEFB(1, "simple", R.string.create_vm_display_backend_simplefb), - VIRTIO_GPU(2, "virtio-gpu", R.string.create_vm_display_backend_virtio_gpu); - - private final int value; - private final String name; - private final @StringRes int stringId; - - DisplayBackend(int value, String name, @StringRes int stringId) { - this.value = value; - this.name = name; - this.stringId = stringId; - } - - @SuppressWarnings("unused") - public int getValue() { - return value; - } - - @SuppressWarnings("unused") - public String getName() { - return name; - } - - @Override - @StringRes - public int getStringId() { - return stringId; - } - - @Override - public boolean isDisplay() { - return stringId != R.string.nullptr; - } +/** + * The legacy either/or display producer: one display, and this said which device made it. + * + *

Superseded by {@link VMScreenConfig} -- the two devices are independent screens now, and a + * VM can have both -- so nothing reads this except {@link VMScreenConfig#migrate}, which reads + * each old config's value exactly once and then drops the key. It is no longer a + * {@code StringEnum} and carries no labels: it is never shown, only decoded.

+ */ +public enum DisplayBackend { + NONE, + SIMPLEFB, + VIRTIO_GPU, } diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/DisplayExporter.java b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/DisplayExporter.java new file mode 100644 index 000000000..282802525 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/DisplayExporter.java @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. +package cn.classfun.droidvm.lib.store.vm; + +import androidx.annotation.StringRes; + +import cn.classfun.droidvm.R; +import cn.classfun.droidvm.lib.store.enums.StringEnum; + +/** + * Who consumes one screen's frames -- the export side of a {@link VMScreenConfig} binding. + *

+ * A screen drives at most one exporter. That is a decision, not a limitation waiting to be + * lifted: the alternative to the old silent race (two sinks configured, VNC wins, the app's + * Surface never gets a binder) was either mirroring or an error, and this picks the error. + * crosvm enforces the same rule on its side and refuses to start a VM with two exporters on + * one screen, so the editor must never write one. + *

+ * The names are persisted, so they are the stable part; they say nothing about which screen + * the binding is on, because that is the key the binding is stored under. + */ +public enum DisplayExporter implements StringEnum { + // Declaration order is the picker's row order: EnumPicker.autoItems() walks + // getEnumConstants() and appends, with no comparator anywhere. So the two exporters lead and + // the sink comes last. Reordering is safe because nothing here is ordinal-shaped -- the + // stored value is name().toLowerCase(), nothing in the tree reads ordinal() or getValue() on + // this enum, and the picker's initial selection is set by configure(cls, value) rather than + // by which constant happens to be first. + NATIVE(1, "native", R.string.create_vm_screen_exporter_native), + VNC(2, "vnc", R.string.create_vm_screen_exporter_vnc), + // Unlike the NONE sentinels of the other persisted enums, this one is a real, selectable + // choice: a screen nobody is watching is a state, not a fault -- crosvm accepts it too. So + // it carries a label instead of R.string.nullptr; a nullptr entry reports isDisplay() == + // false and EnumPicker.autoItems() would drop it from the picker. The label says sink rather + // than none because the screen still exists and still produces frames; they go nowhere. + NONE(0, "none", R.string.create_vm_screen_exporter_none); + + private final int value; + private final String name; + private final @StringRes int stringId; + + DisplayExporter(int value, String name, @StringRes int stringId) { + this.value = value; + this.name = name; + this.stringId = stringId; + } + + @SuppressWarnings("unused") + public int getValue() { + return value; + } + + @SuppressWarnings("unused") + public String getName() { + return name; + } + + @Override + @StringRes + public int getStringId() { + return stringId; + } + + @Override + public boolean isDisplay() { + return stringId != R.string.nullptr; + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/DisplayTransportCap.java b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/DisplayTransportCap.java index 86212e5eb..9de568a5a 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/DisplayTransportCap.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/DisplayTransportCap.java @@ -1,3 +1,54 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. +package cn.classfun.droidvm.lib.store.vm; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; + +import cn.classfun.droidvm.R; +import cn.classfun.droidvm.lib.store.enums.StringEnum; + +/** + * How far up the pipeline one screen's frames are allowed to travel to reach its exporter. + * + *

The transport is not a property of the screen or of the exporter but of the edge between + * them, and it is negotiated: the source says what it can produce (CPU bytes, a dmabuf), the + * exporter says what it can consume, and the highest rung both reach wins. So it is not a thing + * the user picks outright. What the user can have is a ceiling, and that distinction is + * the whole reason this enum reads the way it does.

+ * + *

It only restricts downward. The transport is settled at or below the rung named here, + * so every value is always satisfiable -- CPU copy is the bottom of the ladder and needs nothing + * from either end. "At least GPU copy" would not be: a source that cannot export a dmabuf leaves + * only a silent downgrade (which looks like success and is not) or a loud failure (a VM that + * refuses to start over a preference). A ceiling has neither failure mode. It is also why there is + * no separate "automatic" entry -- the top rung of the offered set already is automatic.

+ * + *

The ladder is not the same on every edge, because the rung above a copy is a different + * mechanism on each. The native display can be lent a render target the guest draws straight into + * ({@link #ZERO}) -- but only when the guest's rendering can be aimed at it, which rules simplefb + * out: its framebuffer is a fixed window of guest memory named in the device tree, and no + * AHardwareBuffer can be made to wrap that. VNC has nowhere to lend a target at all, and its rung + * above a GPU blit is handing the frame to a hardware video encoder instead of an RFB rectangle + * ({@link #GPU_HW}). So the offered set is a function of both ends, and {@link #optionsFor} is + * where that lives.

+ * + *

Rungs that are designed but not built are offered and refused rather than hidden: the ladder + * is easier to understand whole, and a value that appears later must not look like a new feature + * arriving out of nowhere. {@link #isImplemented} says which is which today.

+ */ +public enum DisplayTransportCap implements StringEnum { + /** Always reachable: host memcpy into the sink's own buffer. The bottom of every ladder. */ + CPU(0, "cpu", R.string.create_vm_screen_transport_cpu), + /** A Vulkan blit on the host, which gets a format conversion thrown in for free. */ + GPU(1, "gpu", R.string.create_vm_screen_transport_gpu), + /** Native display only: the sink lends its render target and the content never crosses. */ + ZERO(2, "zero", R.string.create_vm_screen_transport_zero), + /** VNC only: the blit's result goes to a hardware video encoder rather than into RFB. */ + GPU_HW(3, "gpu-hw", R.string.create_vm_screen_transport_gpu_hw); + /** * The width granularity a screen whose stride is exactly {@code width * 4} needs before the * GPU copy can take it, in pixels. @@ -12,6 +63,118 @@ */ public static final long GPU_COPY_WIDTH_ALIGN = 16; + private final int value; + private final String token; + private final @StringRes int stringId; + + DisplayTransportCap(int value, String token, @StringRes int stringId) { + this.value = value; + this.token = token; + this.stringId = stringId; + } + + @SuppressWarnings("unused") + public int getValue() { + return value; + } + + /** + * The stored spelling, which is also the token crosvm's {@code transport-cap=} takes. Lower + * case and hyphenated, unlike the other persisted enums, because this one is written onto a + * command line as well as into the config and one value must not have two spellings. That is + * also why it is parsed by {@link #fromToken} rather than by the generic enum helper, whose + * upper-casing cannot round-trip a hyphen. + */ + @NonNull + public String getToken() { + return token; + } + + @Override + @StringRes + public int getStringId() { + return stringId; + } + + @Override + public boolean isDisplay() { + return stringId != R.string.nullptr; + } + + /** The stored token back to a constant, or null for absent, empty or unrecognised. */ + @Nullable + public static DisplayTransportCap fromToken(@Nullable String stored) { + if (stored == null || stored.isEmpty()) return null; + for (var cap : values()) + if (cap.token.equalsIgnoreCase(stored)) return cap; + return null; + } + + /** + * The ladder this (screen, exporter) edge has, bottom rung first. + * + *

Both ends decide it, which is why the screen is a parameter. The native display's top + * rung is a render target it lends the guest to draw into -- and simplefb cannot be drawn into + * that way at all: its framebuffer is a fixed window of guest memory the guest was told about + * in the device tree, and an AHardwareBuffer cannot be made to wrap it. That rung is not + * "unbuilt" there, it is unreachable, so it is absent rather than greyed. Offering it would be + * describing a choice nobody will ever be able to make.

+ * + *

An exporter with no edge -- nobody is watching the screen -- has no ladder, and the empty + * array is how callers know not to offer one rather than having to ask the question twice.

+ */ + @NonNull + public static DisplayTransportCap[] optionsFor(@NonNull String screenId, + @NonNull DisplayExporter exporter) { + switch (exporter) { + case NATIVE: + return VMScreenConfig.ID_GPU0.equals(screenId) + ? new DisplayTransportCap[]{CPU, GPU, ZERO} + : new DisplayTransportCap[]{CPU, GPU}; + case VNC: + return new DisplayTransportCap[]{CPU, GPU, GPU_HW}; + default: + return new DisplayTransportCap[0]; + } + } + + /** Whether [cap] is one of the rungs this edge has at all. */ + public static boolean isOfferedFor(@NonNull String screenId, + @NonNull DisplayExporter exporter, + @NonNull DisplayTransportCap cap) { + for (var option : optionsFor(screenId, exporter)) + if (option == cap) return true; + return false; + } + + /** + * Whether this build can actually reach [cap] on this edge. + * + */ + public static boolean isImplemented(@NonNull DisplayExporter exporter, + @NonNull DisplayTransportCap cap) { + switch (exporter) { + case NATIVE: + default: + return false; + } + } + + /** The rungs this edge shows but cannot honour yet, in {@link #optionsFor} order. */ + @NonNull + public static DisplayTransportCap[] unimplementedFor(@NonNull String screenId, + @NonNull DisplayExporter exporter) { + var options = optionsFor(screenId, exporter); + var n = 0; + for (var option : options) + if (!isImplemented(exporter, option)) n++; + var out = new DisplayTransportCap[n]; + var i = 0; + for (var option : options) + if (!isImplemented(exporter, option)) out[i++] = option; + return out; + } + /** * Whether a GPU-copy ceiling on this edge will settle on the CPU copy anyway, because the * screen is [width] pixels wide and the blit cannot import a frame that shape. @@ -31,3 +194,21 @@ public static boolean cpuFallbackFromWidth(@NonNull String screenId, return width % GPU_COPY_WIDTH_ALIGN != 0; } + /** + * The ceiling a screen gets when it has not named one: the highest rung this build can + * actually reach on that edge. + * + *

Not the highest rung offered -- that would default every VM to a ceiling nothing can + * satisfy today, which is a promise the negotiation would quietly break. It is the highest + * implemented one, so the default never restricts anything that works, and it rises + * on its own as the rungs land.

+ */ + @NonNull + public static DisplayTransportCap defaultFor(@NonNull String screenId, + @NonNull DisplayExporter exporter) { + var best = CPU; + for (var option : optionsFor(screenId, exporter)) + if (isImplemented(exporter, option)) best = option; + return best; + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/NativeDisplay.java b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/NativeDisplay.java index 04be7e303..36df5f7ea 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/NativeDisplay.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/NativeDisplay.java @@ -9,19 +9,55 @@ import androidx.annotation.NonNull; +import java.nio.charset.StandardCharsets; + /** * Shared naming for the native (crosvm android-display) backend. The daemon (launching crosvm and * hosting the native-display binder) and the UI (looking up the display binder / sending input) - * both derive the per-VM service name and socket paths from here, so they agree and different VMs - * never collide. + * both derive the service names and socket paths from here, so they agree and different VMs never + * collide. + * + * The names the outside world sees hang off one per-VM root, {@link #channelKeyFromId}: a screen's + * display service is that root plus the screen's id. The root is a pure function of the VM's UUID, + * which is why a crosvm that died and restarted re-registers under exactly the name the app is + * still waiting on. + * + * A display service is per screen, not per VM: two screens exporting natively at once would + * otherwise want one servicemanager name for two Surfaces, and a service holds two slots (main + + * cursor), not N. + * + * The input sockets split the same way, but not all of them: the relative pointer alone stays on + * the VM root, because it has no output binding at all -- the guest compositor routes it by focus + * and it walks from one output to the next. The multi-touch and absolute-pointer devices are per + * screen because an absolute coordinate only means anything under one output's geometry; the + * keyboard is per screen for a different reason, which is that input belongs to the scanout. A + * screen's input switch has to be able to turn typing off on that screen, and a VM-wide keyboard + * could not be turned off by any one screen -- so a keyboard the console types into is the + * console's screen's keyboard, and a screen with input off has none at all. * - * The service name doubles as the vmKey: it identifies both the servicemanager entry crosvm - * registers (--android-display-service) and the VM's input-socket set. + * The socket filenames are the one set of names here that is not identity-bearing, and + * they are deliberately terse. A unix socket address holds 107 bytes of path plus a NUL, and this + * app's run directory already spends 35 of them ({@code /data/data/cn.classfun.droidvm/run/}); the + * old {@code droidvm_disp__input_multitouch.sock} came to 106 -- one byte of headroom -- so + * the moment the screen id joined it the per-screen names hit 115 and 111 and crosvm refused the + * whole command line with "path must be shorter than SUN_LEN". Nothing outside this process pair + * ever reads these names: the daemon binds the inode and crosvm is handed the path on the command + * line it is started with, so unlike the service name and the evdev names, which the guest and the + * user's saved mappings key on across reboots, a socket filename can be abbreviated freely. + * {@link #inputSocketPath} therefore builds {@code dvmin_[_]_.sock} out of + * two-to-three-letter tags, worst case 90 bytes, and {@link #requireBindablePath} makes the + * remaining margin a wall rather than a hope -- see its note on what bind(2) does otherwise. */ public final class NativeDisplay { /** * Input channels. Each maps to one crosvm {@code --input } virtio-input device and one + * unix socket, and the UI routes to whichever the current {@code InputMode} selects + * (multi-touch, relative mouse, or absolute single-touch tablet). Ordinals are the wire * channel ids shared with the daemon's InputHandler; append new channels, never renumber. + * + *

A channel is not by itself a device any more: {@link #isPerScreen} says whether the VM + * has one of them or one per screen, so the daemon binds a socket per (screen, channel) pair + * that exists rather than {@link #CHANNEL_COUNT} of them.

*/ public static final int MULTITOUCH = 0; public static final int KEYBOARD = 1; @@ -43,27 +79,241 @@ public final class NativeDisplay { /** Per-attach random token; the UI only accepts a broadcast carrying the nonce it requested. */ public static final String EXTRA_NONCE = "nonce"; + // Socket filename tags per channel (index == channel constant). "ms"/"tab" are our tags; the + // crosvm device kinds they pair with are "mouse" (relative) and "absolute-mouse". Short + // because the whole path has to fit sun_path (see the class note); still self-describing, + // because the next person reading `ls run/` deserves to know which inode is the touchscreen. + private static final String[] CHANNEL_TAGS = {"mt", "kbd", "ms", "tab"}; + + /** + * Socket filename tag per screen, positionally paired with {@link VMScreenConfig#IDS}. A + * screen id is a word ("simplefb"); a socket name has a couple of bytes to spend on it. The + * static check below is the wall a third screen walks into: adding an id without adding its + * tag fails at class load rather than minting a name that overflows or collides. + */ + private static final String[] SCREEN_TAGS = {"g0", "sfb"}; + + static { + if (SCREEN_TAGS.length != VMScreenConfig.IDS.length) + throw new IllegalStateException("every screen id needs an input-socket tag"); + } + private static final String RUN_PATH = pathJoin(DATA_DIR, "run"); + /** Prefix every name built here starts with; {@link #vmIdFromServiceName} reads it back. */ + private static final String NAME_PREFIX = "droidvm_disp_"; + + /** Prefix of the input socket filenames; short, and not a name anything looks up by. */ + private static final String SOCKET_PREFIX = "dvmin_"; + + /** + * Bytes of {@code sockaddr_un.sun_path} on Linux -- 108, of which the last must be the NUL, + * so 107 is the longest bindable path. Not a tunable: it is the kernel's array size. + */ + public static final int SUN_PATH_SIZE = 108; + + /** The longest path {@link #requireBindablePath} will pass: {@link #SUN_PATH_SIZE} less NUL. */ + public static final int MAX_UNIX_PATH = SUN_PATH_SIZE - 1; + private NativeDisplay() { } - /** Per-VM crosvm display service name; also used as the vmKey for input sockets. */ + /** + * The VM's display-channel root: the stem every per-screen service name is built on, and the + * name a pre-screens build registered on its own. Stable across boots because the VM's UUID + * is. Taken as a raw id rather than a config because half its callers only have the id, out + * of an Intent extra or a service name being read back. + */ + @NonNull + public static String channelKeyFromId(@NonNull String vmId) { + return fmt("%s%s", NAME_PREFIX, sanitize(vmId)); + } + + /** + * The servicemanager name crosvm registers for one screen's native display -- the value of + * that screen's {@code --android-display-service name=}, and what the UI looks up. + * + * The screen id rides in the name rather than in a separate field because the name is the + * whole identity of a channel on both sides of the binder, and because it has to survive a + * crosvm restart unchanged: it is derived, never allocated. The charset stays the one + * {@link #sanitize} allows, so the name is legal both as a binder service name and inside a + * socket filename. + */ + @NonNull + public static String serviceName(@NonNull VMConfig config, @NonNull String screenId) { + return serviceNameFromId(config.getId().toString(), screenId); + } + + /** Same as {@link #serviceName(VMConfig, String)} but from a raw VM id. */ + @NonNull + public static String serviceNameFromId(@NonNull String vmId, @NonNull String screenId) { + return fmt("%s_%s", channelKeyFromId(vmId), sanitize(screenId)); + } + + /** + * The VM id a name built here belongs to, or an empty string when the name is not ours. + * + * The daemon uses this to decide whether a display binder is worth waiting for, so it has to + * be the exact inverse of the two builders above -- which is why it lives next to them + * instead of being a prefix-strip written out again at the call site. + */ + @NonNull + public static String vmIdFromServiceName(@NonNull String serviceName) { + if (!serviceName.startsWith(NAME_PREFIX)) return ""; + var rest = serviceName.substring(NAME_PREFIX.length()); + for (var screenId : VMScreenConfig.IDS) { + var suffix = fmt("_%s", sanitize(screenId)); + if (rest.endsWith(suffix)) + return rest.substring(0, rest.length() - suffix.length()); + } + return rest; + } + + /** + * Whether [channel]'s device belongs to one screen rather than to the whole VM. + * + *

The two absolute devices are: their coordinates are read against one output's geometry, + * so a VM with two screens needs two of each and the guest has to be told by hand which is + * which. The keyboard is too, on the different ground that input is a property of the scanout: + * the screen's input switch governs it, so it cannot be one device shared by screens that + * disagree about whether input is on. Only the relative pointer is left VM-wide -- it has no + * output binding, and the guest compositor sends it wherever focus is.

+ * + *

This one predicate is what makes a channel per screen everywhere at once: it picks the + * socket filename ({@link #inputSocketPath}), the daemon's slot key, and therefore whether the + * screen the console names reaches the write at all. The UI already sends its screen id on + * every channel, so a channel moving across this line needs no change on that side.

+ */ + public static boolean isPerScreen(int channel) { + return channel == MULTITOUCH || channel == TABLET || channel == KEYBOARD; + } + + /** + * The socket filename tag for [screenId], or the sanitized id itself if the screen is one + * {@link #SCREEN_TAGS} has never heard of -- a long name the length check will catch, which is + * the failure worth having over a short one that might collide with another screen's tag. + */ + @NonNull + private static String screenTag(@NonNull String screenId) { + for (int i = 0; i < VMScreenConfig.IDS.length; i++) + if (VMScreenConfig.IDS[i].equals(screenId)) return SCREEN_TAGS[i]; + return sanitize(screenId); + } + + /** + * The socket path for [channel] on [screenId] of [vmId]: {@code dvmin___.sock}, + * with the screen tag left out entirely for the VM-wide channel -- so passing the empty + * screen id for the relative pointer is exact rather than a placeholder, and no screen a + * console could name reaches its inode. + * + *

The daemon binds these and crosvm's {@code --input ...[path=]} connects to them, and the + * two sides agree because they call this one function rather than each composing the name and + * the directory themselves.

+ * + *

Terse on purpose, and safe to be terse: unlike the service name and the evdev names, this + * string is born and dies inside one VM start -- see the class note for what the long form + * cost. Worst case here is 90 bytes of the 107 a unix socket address holds; the margin is + * asserted in the tests and enforced by {@link #requireBindablePath} at the bind.

+ */ + @NonNull + public static String inputSocketPath(@NonNull String vmId, @NonNull String screenId, + int channel) { + var screen = isPerScreen(channel) ? fmt("_%s", screenTag(screenId)) : ""; + return pathJoin(RUN_PATH, fmt("%s%s%s_%s.sock", + SOCKET_PREFIX, sanitize(vmId), screen, CHANNEL_TAGS[channel])); + } + + /** + * Returns [path] if a unix socket can actually be bound to it, and throws naming the path and + * its length if not. + * + *

This exists because the two ends disagree about what to do with an over-long path, and + * both answers are bad. crosvm refuses the command line outright ("path must be shorter than + * SUN_LEN") and the VM never starts. bind(2) as this daemon reaches it does the opposite: the + * path is copied into a 108-byte {@code sun_path} and silently truncated, so the + * daemon binds some other inode, logs a successful pre-listen, and waits forever for a crosvm + * that was told the untruncated name -- run/ on the test phone still held two of those stubs, + * {@code ..._simplefb_input_multito} and {@code ..._simplefb_input_tablet.}, as the only trace + * that anything had gone wrong. So the length is checked here, before the syscall, and a name + * that grows past the limit hits a wall with the number in the message instead of a mystery.

+ * + *

Measured in bytes, not chars: the kernel copies bytes, and {@link #sanitize} keeps the + * two equal only as long as every name it is fed is ASCII.

+ */ @NonNull - public static String serviceName(@NonNull VMConfig config) { - return serviceNameFromId(config.getId().toString()); + public static String requireBindablePath(@NonNull String path) { + int len = path.getBytes(StandardCharsets.UTF_8).length; + if (len > MAX_UNIX_PATH) + throw new IllegalArgumentException(fmt( + "unix socket path is %d bytes, over the %d sun_path allows: %s", + len, MAX_UNIX_PATH, path)); + return path; } - /** Same as {@link #serviceName(VMConfig)} but from a raw VM id (e.g. an Intent extra). */ + /** + * The evdev name crosvm gives [screenId]'s multi-touch device -- what the guest sees as the + * touchscreen's name, and the whole of its identity there. + * + *

Neither evdev nor HID has a field for "I belong to output N", so every guest OS maps a + * touchscreen to an output by the device's name: kwin stores it by name, + * {@code xinput map-to-output} takes it by name, Windows' Tablet PC setup remembers the one + * it was pointed at. That makes the name the only lever there is, and it has to be a pure + * function of the screen and never change -- rename it and the user's mapping silently stops + * matching anything, with no error to notice.

+ * + *

Its absolute-pointer sibling is named by {@link #tabletDeviceName} for the same reason + * and on the same terms.

+ */ @NonNull - public static String serviceNameFromId(@NonNull String vmId) { - return fmt("droidvm_disp_%s", sanitize(vmId)); + public static String touchDeviceName(@NonNull String screenId) { + return fmt("DroidVM Touch (%s)", screenId); } - /** The socket path crosvm connects to for [vmKey]'s [channel]. Must match across all callers. */ + /** + * The evdev name crosvm gives [screenId]'s absolute-pointer (tablet) device. + * + *

Everything {@link #touchDeviceName} says applies here unchanged -- an absolute pointer is + * as much a per-output device as a touchscreen, and the guest maps it to an output by name in + * exactly the same places. It used to have no name at all, because crosvm's + * {@code absolute-mouse} option had no {@code name} field and its option enum rejects unknown + * keys, so the device fell back to crosvm's generated "Crosvm Virtio Absolute Mouse <idx>" + * -- an index that counts emission order and therefore moves when another screen's input is + * switched off, which is the one thing a mapping key must never do. crosvm takes the field + * now, so the tablet is pinnable on the same terms as the touchscreen.

+ * + *

Cross-repo seam. This string is produced in two places. A natively exported screen's + * tablet is the {@code --input absolute-mouse} the daemon emits with this name. A VNC-exported + * screen's tablet is built by crosvm itself, behind that screen's VNC server, and crosvm names + * it by reproducing this format -- there is no command-line key carrying it, so the format is + * the contract. Changing the format here means changing crosvm's VNC device setup in the same + * breath; changing only one silently unpins every guest-side mapping on the other. Which + * screens get which is {@code CrosvmBackendInstance.nativeInputScreens}. The same seam applies + * to {@link #keyboardDeviceName}.

+ */ + @NonNull + public static String tabletDeviceName(@NonNull String screenId) { + return fmt("DroidVM Tablet (%s)", screenId); + } + + /** + * The evdev name [screenId]'s keyboard carries. + * + *

A keyboard is not an absolute device and the guest binds no output to it, so unlike its + * two siblings this name is not what a mapping keys on. It is still derived per screen, for + * two reasons. The guest lists these side by side and a user looking at several identical + * "DroidVM Keyboard" entries cannot tell which screen's switch turns which one off; and the + * name is the only thing distinguishing them, since a keyboard advertises nothing else that + * differs.

+ * + *

Cross-repo seam, on the same terms as {@link #tabletDeviceName} and in both + * directions: the daemon emits this string in {@code --input keyboard[...,name=]} for a + * natively exported screen, and crosvm builds a VNC-exported screen's keyboard itself and + * names it by reproducing this format. Two producers, one format, and no command-line key + * carrying it between them.

+ */ @NonNull - public static String inputSocketPath(@NonNull String vmKey, int channel) { - return pathJoin(RUN_PATH, fmt("%s_input_%s.sock", sanitize(vmKey), KINDS[channel])); + public static String keyboardDeviceName(@NonNull String screenId) { + return fmt("DroidVM Keyboard (%s)", screenId); } /** Keep socket/service names to a filesystem- and binder-safe charset. */ diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMConfig.java b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMConfig.java index deff965f4..ec48412ad 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMConfig.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMConfig.java @@ -105,10 +105,21 @@ public static VMConfig createWithCustomizeDefaults(@NonNull Context context) { var gpu = VMScreenConfig.of(item, VMScreenConfig.ID_GPU0); gpu.setEnabled(false); - gpu.setExporter(DisplayExporter.NONE); + // Set even though the screen is off, because this is what the editor shows the moment the + // user turns virtio-gpu on -- the row's own default stopped being reachable when a new VM + // started going through loadConfig like an existing one. Nothing is exported until the + // screen is enabled: save() writes NONE for a screen that is off, and the backend emits + // exporters only for enabled screens. + gpu.setExporter(VMScreenConfig.NEW_VM_DEFAULT_EXPORTER); gpu.setTransportCap(DisplayTransportCap.defaultFor( - VMScreenConfig.ID_GPU0, DisplayExporter.NATIVE)); + VMScreenConfig.ID_GPU0, VMScreenConfig.NEW_VM_DEFAULT_EXPORTER)); gpu.setInputEnabled(true); + // Written even though the screen is off and its exporter is not VNC, for the same reason + // the exporter above is: the editor loads this config over its rows, so what it shows the + // moment the user switches this screen to VNC is what is written here. The two screens get + // different ports because they can be exported at once and two servers may not share one. + gpu.setVncHost(VMScreenConfig.NEW_VM_DEFAULT_VNC_HOST); + gpu.setVncPort(VMScreenConfig.newVmDefaultVncPort(VMScreenConfig.ID_GPU0)); gpu.setWidth(VMScreenConfig.DEFAULT_WIDTH); gpu.setHeight(VMScreenConfig.DEFAULT_HEIGHT); gpu.setRefreshRate(VMScreenConfig.DEFAULT_REFRESH_RATE); @@ -117,10 +128,12 @@ public static VMConfig createWithCustomizeDefaults(@NonNull Context context) { var simpleFb = VMScreenConfig.of(item, VMScreenConfig.ID_SIMPLEFB); simpleFb.setEnabled(true); - simpleFb.setExporter(DisplayExporter.NATIVE); + simpleFb.setExporter(VMScreenConfig.NEW_VM_DEFAULT_EXPORTER); simpleFb.setTransportCap(DisplayTransportCap.defaultFor( - VMScreenConfig.ID_SIMPLEFB, DisplayExporter.NATIVE)); + VMScreenConfig.ID_SIMPLEFB, VMScreenConfig.NEW_VM_DEFAULT_EXPORTER)); simpleFb.setInputEnabled(true); + simpleFb.setVncHost(VMScreenConfig.NEW_VM_DEFAULT_VNC_HOST); + simpleFb.setVncPort(VMScreenConfig.newVmDefaultVncPort(VMScreenConfig.ID_SIMPLEFB)); simpleFb.setWidth(VMScreenConfig.DEFAULT_WIDTH); simpleFb.setHeight(VMScreenConfig.DEFAULT_HEIGHT); simpleFb.setPollHz(VMScreenConfig.NEW_VM_DEFAULT_POLL_HZ); diff --git a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMScreenConfig.java b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMScreenConfig.java index 1efe3543a..9e4b41d9d 100644 --- a/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMScreenConfig.java +++ b/app/src/main/java/cn/classfun/droidvm/lib/store/vm/VMScreenConfig.java @@ -1,5 +1,427 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. +package cn.classfun.droidvm.lib.store.vm; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; + +import java.util.ArrayList; +import java.util.List; + +import cn.classfun.droidvm.R; +import cn.classfun.droidvm.lib.store.base.DataItem; +import cn.classfun.droidvm.lib.store.enums.Enums; + +/** + * One entry of a VM config's "screens" object -- one screen, and the one exporter bound to it. + * + *

A screen is a display device the VM has: {@code gpu-0} is the virtio-gpu device's screen, + * {@code simplefb} is the simplefb device's, and the two are independent devices rather than two + * settings of one. The ids are exactly the tokens crosvm's {@code screen=} takes, because they + * are handed to it verbatim; they are also the {@code stable_id} the app stores when the user + * picks a screen to open, so they must never be renamed.

+ * + *

The entry is keyed by that id rather than sitting in an array, so "one binding per screen" + * -- which crosvm rejects a command line for violating -- cannot be expressed wrongly here in + * the first place.

+ * + *

{@code enabled} says the VM has that display device at all -- for {@code gpu-0} that is the + * whole virtio-gpu device, renderer included, and the renderer is a setting inside it rather than + * a switch of its own. The exporter says who watches the screen; a screen with no exporter is a + * legal, ordinary state (the guest still has the device), and crosvm accepts it too.

+ */ +public final class VMScreenConfig { + /** Key of the screens object on the VM config. */ + public static final String KEY = "screens"; + + /** The virtio-gpu device's screen. Only exists when the VM has a GPU. */ + public static final String ID_GPU0 = "gpu-0"; + /** The simplefb device's screen; its geometry is fixed by the device tree. */ + public static final String ID_SIMPLEFB = "simplefb"; + /** Every screen id, in the order the UI lists them. Iteration order is part of the schema. */ + public static final String[] IDS = {ID_GPU0, ID_SIMPLEFB}; + + /** Sub-object holding this screen's VNC server settings, when its exporter is VNC. */ + private static final String KEY_VNC = "vnc"; + /** + * Whether this screen gets its own absolute input devices. Absent means on, which is how + * every config written before this key existed keeps the devices it already had: the default + * carries the migration, so nothing on disk has to be rewritten to gain it. + */ + private static final String KEY_INPUT_ENABLED = "input_enabled"; + /** Ceiling on the transport negotiated between this screen and its exporter. */ + private static final String KEY_TRANSPORT_CAP = "transport_cap"; + + // Geometry. Every screen has a size; only the virtio-gpu screen has a mode to put a refresh + // rate and a DPI in, and only simplefb has a poll rate, because that is the difference between + // a device the guest programs and a block of memory the host watches. + private static final String KEY_WIDTH = "width"; + private static final String KEY_HEIGHT = "height"; + private static final String KEY_REFRESH_RATE = "refresh_rate"; + private static final String KEY_DPI_H = "dpi_h"; + private static final String KEY_DPI_V = "dpi_v"; + private static final String KEY_POLL_HZ = "poll_hz"; + + /** The VM-level keys {@link #migrateGeometry} folds into the screens, then drops. */ + private static final String[] LEGACY_GEOMETRY = { + "display_width", "display_height", "display_refresh_rate", "display_dpi_h", "display_dpi_v" + }; + /** + * The VM-level accelerator switch, folded into the gpu-0 screen's own switch by + * {@link #migrateGpuDevice} and then dropped. + */ + private static final String LEGACY_GPU_ENABLED = "gpu_enabled"; + + public static final long DEFAULT_WIDTH = 1280; + public static final long DEFAULT_HEIGHT = 720; + public static final long DEFAULT_REFRESH_RATE = 120; + public static final long DEFAULT_DPI = 160; + /** + * What the simplefb bridge polled at for as long as the rate was not settable, so a screen + * that never names one behaves as it always did. Mirrors crosvm's DEFAULT_SIMPLEFB_POLL_HZ. + */ + public static final long DEFAULT_POLL_HZ = 30; /** Initial SimpleFB polling rate shown when creating a VM. */ public static final long NEW_VM_DEFAULT_POLL_HZ = 60; + + /** + * What a screen on a brand-new VM is bound to, in the one place both writers can see it. + * + *

Two of them write it and they must agree: {@link VMConfig#createWithCustomizeDefaults} + * materialises the config the editor is then handed, and the editor's own screen rows carry a + * default for the case where that config says nothing. They disagreed once -- the config said + * NONE while the row said NATIVE -- and because the config is loaded over the row, turning + * virtio-gpu on showed a screen bound to nothing. Neither side is wrong to have a default; the + * fix is for there to be one value. + * + *

NATIVE rather than VNC because the viewer for it is this app, already installed: it is + * the only exporter whose first boot can be looked at without setting something up first. This + * is not {@link #getExporter}'s fallback, which answers a different question -- what a stored + * config that does not mention an exporter meant -- and stays NONE. + */ + public static final DisplayExporter NEW_VM_DEFAULT_EXPORTER = DisplayExporter.NATIVE; + /** + * What a brand-new VM's VNC server is told to listen on, in the one place both writers can + * see it -- {@link VMConfig#createWithCustomizeDefaults} and the editor's own screen row -- + * for exactly the reason {@link #NEW_VM_DEFAULT_EXPORTER} is shared: the config is loaded over + * the row, so a second opinion here would be the losing one and the disagreement invisible. + * + *

The loopback rather than the wildcard. A screen that is reachable from whatever network + * the phone is on the moment the VM first boots is not a default's decision to make; widening + * it is one pick in the host menu, and the app's own console dials the phone itself either + * way.

+ */ + public static final String NEW_VM_DEFAULT_VNC_HOST = "127.0.0.1"; + /** The virtio-gpu screen's port on a new VM: RFB display :0, where a client looks first. */ + public static final long NEW_VM_DEFAULT_VNC_PORT_GPU0 = 5900; + /** + * The simplefb screen's port on a new VM: RFB display :9. + * + *

Nine displays up rather than one, because the two are defaults and not assignments: both + * screens can be bound to VNC at once, crosvm refuses a command line whose servers share a + * port, and the editor refuses the save before that (see {@code validateNoPortCollision}). The + * gap also leaves :1..:8 to a user handing them out by hand.

+ */ + public static final long NEW_VM_DEFAULT_VNC_PORT_SIMPLEFB = 5909; + public static final long MIN_POLL_HZ = 1; + /** + * crosvm's MAX_SIMPLEFB_POLL_HZ. A sanity bound on a knob whose cost is linear in it, not a + * claim about what a panel can show -- above this the watcher asks for more work than anything + * downstream can use. + */ + public static final long MAX_POLL_HZ = 240; + public final String id; + public final DataItem item; + + public VMScreenConfig(@NonNull String id, @NonNull DataItem item) { + this.id = id; + this.item = item; + } + + /** Whether this screen's display device is configured for the VM. */ + public boolean isEnabled() { + return item.optBoolean("enabled", false); + } + + public void setEnabled(boolean enabled) { + item.set("enabled", enabled); + } + + /** + * Who watches this screen. Absent reads as NONE, and stays reading as NONE even though a new + * screen now comes up defaulted to NATIVE. + * + *

Those are two different questions and only one of them is here. What a new VM gets is the + * picker's default, set where the row is built; this is what a file that does not say means. + * Nothing this app writes leaves the key out -- {@code ScreenBindingRow.save} calls + * {@link #setExporter} unconditionally on the entry {@link #of} has just created, and + * {@link #migrateBindings} writes it for every config from before the screens split -- so + * moving the fallback would not change what any VM comes up with. It would only change how a + * hand-edited file, or a token this build cannot parse, is read, and it would change it into + * a display service and a pair of input devices bound to a screen whose file never asked for + * either, because everything downstream tests {@code != NONE}.

+ */ + @NonNull + public DisplayExporter getExporter() { + return Enums.optEnum(item, "exporter", DisplayExporter.NONE); + } + + public void setExporter(@NonNull DisplayExporter exporter) { + item.set("exporter", exporter); + } + + /** + * Whether this screen gets its own multi-touch and absolute-pointer devices. + * + *

Absolute coordinates only mean anything under one output's geometry, so those two + * devices are per screen; the VM-wide keyboard and relative pointer have no output binding at + * all and are unaffected by this switch. Turning it off is turning the two devices off: the + * set of {@code --input} devices is fixed when crosvm starts, so this takes effect on the + * VM's next start, not on the running one.

+ * + *

Where the screen is exported over VNC the switch means one thing more, without meaning + * anything different: those two devices are half crosvm's now -- it builds that binding's + * tablet and its own keyboard itself -- so the daemon spells the same off/on as + * {@code view-only=true|false} on that screen's {@code --vnc-server}, and off makes crosvm + * build neither and drop RFB pointer and key events. Which is the answer the user was already + * asking for: a screen to watch and not touch, from the app's console and from a third-party + * client alike.

+ * + *

Defaults to on, and to on for a config that predates the key -- the devices existed + * before it did.

+ */ + public boolean isInputEnabled() { + return item.optBoolean(KEY_INPUT_ENABLED, true); + } + + public void setInputEnabled(boolean enabled) { + item.set(KEY_INPUT_ENABLED, enabled); + } + + /** + * The ceiling on this screen's transport to its exporter; see {@link DisplayTransportCap}. + * + *

Read against the exporter, because the ladder is the exporter's: a value stored under one + * exporter and read back under another may name a rung that exporter does not have, and the + * answer then is that exporter's default rather than a rung nobody can climb. The stored value + * is left alone -- switching the exporter to look and back must not lose the choice.

+ */ + @NonNull + public DisplayTransportCap getTransportCap() { + var exporter = getExporter(); + var stored = DisplayTransportCap.fromToken(item.optString(KEY_TRANSPORT_CAP, "")); + if (stored != null && DisplayTransportCap.isOfferedFor(id, exporter, stored)) return stored; + return DisplayTransportCap.defaultFor(id, exporter); + } + + public void setTransportCap(@NonNull DisplayTransportCap cap) { + // The lower-case token, not the enum constant: this same string goes on crosvm's command + // line, and one value must not have two spellings. + item.set(KEY_TRANSPORT_CAP, cap.getToken()); + } + + /** This screen's width in pixels. */ + public long getWidth() { + return item.optLong(KEY_WIDTH, DEFAULT_WIDTH); + } + + public void setWidth(long width) { + item.set(KEY_WIDTH, width); + } + + /** This screen's height in pixels. */ + public long getHeight() { + return item.optLong(KEY_HEIGHT, DEFAULT_HEIGHT); + } + + public void setHeight(long height) { + item.set(KEY_HEIGHT, height); + } + + /** + * The mode's refresh rate, in Hz. Virtio-GPU only: it reaches the guest through the scanout's + * mode, and simplefb has no mode to carry one -- the device tree says nothing about time. + */ + public long getRefreshRate() { + return item.optLong(KEY_REFRESH_RATE, DEFAULT_REFRESH_RATE); + } + + public void setRefreshRate(long hz) { + item.set(KEY_REFRESH_RATE, hz); + } + + /** Horizontal DPI of the mode. Virtio-GPU only, for the same reason as the refresh rate. */ + public long getDpiH() { + return item.optLong(KEY_DPI_H, DEFAULT_DPI); + } + + public void setDpiH(long dpi) { + item.set(KEY_DPI_H, dpi); + } + + /** Vertical DPI of the mode. Virtio-GPU only. */ + public long getDpiV() { + return item.optLong(KEY_DPI_V, DEFAULT_DPI); + } + + public void setDpiV(long dpi) { + item.set(KEY_DPI_V, dpi); + } + + /** + * How many times a second the host looks at the simplefb framebuffer -- this screen's answer + * to "refresh rate", and the only one it can give. + * + *

Nothing announces a frame here: the guest maps the region write-combining and no write + * traps, so the rate the host samples at is the only thing that decides when a picture exists. + * It is a property of the host's watcher, not of the device the guest sees -- the guest cannot + * tell what it is set to -- which is why it lives on this screen and the GPU screen's refresh + * rate, a real mode field the guest is told about, does not.

+ */ + public long getPollHz() { + return item.optLong(KEY_POLL_HZ, DEFAULT_POLL_HZ); + } + + public void setPollHz(long hz) { + item.set(KEY_POLL_HZ, hz); + } + + /** True for the screen the virtio-gpu device provides, as opposed to simplefb's. */ + public boolean isGpu() { + return ID_GPU0.equals(id); + } + + /** The port a brand-new VM's [id] screen is given; see the two constants it picks between. */ + public static long newVmDefaultVncPort(@NonNull String id) { + return ID_GPU0.equals(id) + ? NEW_VM_DEFAULT_VNC_PORT_GPU0 : NEW_VM_DEFAULT_VNC_PORT_SIMPLEFB; + } + + /** This screen's VNC sub-object, created if it is not there yet. */ + @NonNull + public DataItem vnc() { + var vnc = item.opt(KEY_VNC, (DataItem) null); + if (vnc == null || !vnc.is(DataItem.Type.OBJECT)) { + item.set(KEY_VNC, DataItem.newObject()); + vnc = item.get(KEY_VNC); + } + return vnc; + } + + /** Listen address; empty means crosvm's own default (the IPv4 wildcard). */ + @NonNull + public String getVncHost() { + return str(vnc().optString("host", "")); + } + + public void setVncHost(@NonNull String host) { + vnc().set("host", host); + } + + /** Listen port, or -1 when one has not been assigned yet (the daemon picks one on start). */ + public long getVncPort() { + return vnc().optLong("port", -1); + } + + public void setVncPort(long port) { + vnc().set("port", port); + } + + public boolean isVncPasswordAuth() { + return vnc().optBoolean("password_auth", false); + } + + public void setVncPasswordAuth(boolean auth) { + vnc().set("password_auth", auth); + } + + @NonNull + public String getVncPassword() { + return str(vnc().optString("password", "")); + } + + public void setVncPassword(@NonNull String password) { + vnc().set("password", password); + } + + @StringRes + public int getNameStringId() { + return isGpu() ? R.string.create_vm_screen_gpu0 : R.string.create_vm_screen_simplefb; + } + + /** Human-facing name of the screen: "Virtio-GPU screen", "SimpleFB screen". */ + @NonNull + public String getDisplayName(@NonNull Context ctx) { + return ctx.getString(getNameStringId()); + } + + @NonNull + private static String str(@Nullable String s) { + return s == null ? "" : s; + } + + /** The screens object on {@code config}, created if absent. */ + @NonNull + private static DataItem screensOf(@NonNull DataItem config) { + var screens = config.opt(KEY, (DataItem) null); + if (screens == null || !screens.is(DataItem.Type.OBJECT)) { + config.set(KEY, DataItem.newObject()); + screens = config.get(KEY); + } + return screens; + } + + /** The entry for [id], created empty if the config has none yet. */ + @NonNull + public static VMScreenConfig of(@NonNull DataItem config, @NonNull String id) { + var screens = screensOf(config); + var entry = screens.opt(id, (DataItem) null); + if (entry == null || !entry.is(DataItem.Type.OBJECT)) { + screens.set(id, DataItem.newObject()); + entry = screens.get(id); + } + return new VMScreenConfig(id, entry); + } + + /** The entry for [id], or null when the config does not describe that screen. */ + @Nullable + public static VMScreenConfig find(@NonNull DataItem config, @NonNull String id) { + var screens = config.opt(KEY, (DataItem) null); + if (screens == null || !screens.is(DataItem.Type.OBJECT)) return null; + var entry = screens.opt(id, (DataItem) null); + if (entry == null || !entry.is(DataItem.Type.OBJECT)) return null; + return new VMScreenConfig(id, entry); + } + + /** + * Whether the VM has the virtio-gpu device at all -- which is exactly the gpu-0 screen's + * switch, because the switch is the device. + * + *

There used to be a second answer, a VM-level {@code gpu_enabled}, and the pair could + * disagree: a GPU with the screen off was meant to be a renderer that scans out nothing. It + * never worked -- no desktop ever came up on it -- so the two collapsed into one, and the + * renderer became a setting inside the device rather than a switch beside it. Everything that + * used to ask "is there a GPU" asks here.

+ */ + public static boolean hasGpuDevice(@NonNull DataItem config) { + var gpu0 = find(config, ID_GPU0); + return gpu0 != null && gpu0.isEnabled(); + } + + /** Every screen that exists and has an exporter bound to it, in {@link #IDS} order. */ + @NonNull + public static List boundOf(@NonNull DataItem config) { + var out = new ArrayList(); + for (var screen : listOf(config)) + if (screen.isEnabled() && screen.getExporter() != DisplayExporter.NONE) + out.add(screen); + return out; + } + /** * *

For the host-process settings that belong to that path rather than to one screen -- the @@ -10,3 +432,205 @@ return false; } + /** + * Whether this screen gets its own {@code multi-touch} + absolute-pointer pair when the VM + * starts: the device has to exist, something has to be watching it, and the switch has to be + * on. A screen nobody exports has no console to send absolute input from, so devices for it + * would be devices nothing can ever write to. + * + *

Both devices, but not both from here: on a VNC-exported screen this is what the daemon + * turns into {@code view-only=false}, and crosvm builds the tablet. Which half is whose is + * {@code CrosvmBackendInstance.touchscreenScreens}/{@code socketTabletScreens}; this predicate + * is the same question for both and stays one.

+ */ + public boolean hasAbsoluteInput() { + return isEnabled() && getExporter() != DisplayExporter.NONE && isInputEnabled(); + } + + /** Every screen that gets its own absolute input devices, in {@link #IDS} order. */ + @NonNull + public static List absoluteInputOf(@NonNull DataItem config) { + var out = new ArrayList(); + for (var screen : listOf(config)) + if (screen.hasAbsoluteInput()) out.add(screen); + return out; + } + + /** Every screen the config describes, in {@link #IDS} order. */ + @NonNull + public static List listOf(@NonNull DataItem config) { + var out = new ArrayList(); + for (var id : IDS) { + var screen = find(config, id); + if (screen != null) out.add(screen); + } + return out; + } + + /** + * Brings a config up to the current screens schema. Two folds, each gated on its own evidence + * that it has not run yet, so a config from any generation lands in the same place. + */ + static void migrate(@NonNull DataItem config) { + // Asked before anything runs, because the first fold is what makes it false. + var preScreens = config.opt(KEY, (DataItem) null) == null; + migrateBindings(config); + migrateGeometry(config); + // Last, because the two above still read gpu_enabled to work out what the old config + // meant, and this is the one that takes it away. + migrateGpuDevice(config, preScreens); + } + + /** + * Folds the legacy VM-level display keys into per-screen bindings, once, on load. + * + *

The old model had one display: {@code display_backend} chose which device produced it + * and {@code native_display_enabled} / {@code vnc_enabled} chose who consumed it, with the + * two consumers mutually exclusive because crosvm kept whichever sink opened first. The new + * model has two independent devices, each carrying its own binding, so the enum becomes two + * enables and the consumer booleans become one exporter per screen.

+ * + *

Unlike {@link VMConfig#migrateBoot} this drops the legacy keys rather than leaving them + * for an older build to read: they no longer describe a state this schema can be in (two + * screens bound at once has no {@code display_backend} value), so leaving them would leave a + * second, disagreeing answer on disk for the same question.

+ */ + private static void migrateBindings(@NonNull DataItem config) { + if (config.opt(KEY, (DataItem) null) != null) return; // already the new shape + + var displayEnabled = config.optBoolean("display_enabled", false); + var backend = Enums.optEnum(config, "display_backend", DisplayBackend.NONE); + var wantNative = config.optBoolean("native_display_enabled", false); + var wantVnc = config.optBoolean("vnc_enabled", false); + var gpuEnabled = config.optBoolean(LEGACY_GPU_ENABLED, false); + + // Which screen the old config named. display_enabled gated everything, so a VM with it + // off named no screen no matter what the backend said. + String bound = null; + if (displayEnabled && backend == DisplayBackend.VIRTIO_GPU) bound = ID_GPU0; + else if (displayEnabled && backend == DisplayBackend.SIMPLEFB) bound = ID_SIMPLEFB; + + // `--vnc-server` was emitted from vnc_enabled alone -- buildVncCommand never looked at + // display_enabled -- so "display off, VNC on, GPU on" was a working VM: crosvm gave the + // GPU device its default display and the VNC server showed it. Keep it working by + // binding to the screen crosvm's own compat rule would have picked for an exporter that + // named none: gpu-0 when there is a GPU. With no GPU there was no device to open and the + // server never served anything, so there is nothing to carry over. + if (bound == null && wantVnc && !wantNative && gpuEnabled) bound = ID_GPU0; + + var gpu0 = of(config, ID_GPU0); + var fb = of(config, ID_SIMPLEFB); + gpu0.setEnabled(ID_GPU0.equals(bound)); + fb.setEnabled(ID_SIMPLEFB.equals(bound)); + gpu0.setExporter(DisplayExporter.NONE); + fb.setExporter(DisplayExporter.NONE); + + var exporter = wantNative ? DisplayExporter.NATIVE + : wantVnc ? DisplayExporter.VNC : DisplayExporter.NONE; + if (bound != null && exporter != DisplayExporter.NONE) + of(config, bound).setExporter(exporter); + + // The VNC settings follow the binding, and when nothing is bound they still have to land + // somewhere or a port and a password the user chose are lost. Park them on the screen the + // old backend named, or -- with no backend at all -- on the one crosvm would have + // defaulted an unscreened exporter to, so re-enabling the exporter finds them in place. + var vncHome = bound != null ? bound : gpuEnabled ? ID_GPU0 : ID_SIMPLEFB; + var home = of(config, vncHome); + home.setVncHost(str(config.optString("vnc_host", ""))); + home.setVncPort(config.optLong("vnc_port", -1)); + home.setVncPasswordAuth(config.optBoolean("vnc_password_auth", false)); + home.setVncPassword(str(config.optString("vnc_password", ""))); + + config.remove("display_enabled"); + config.remove("display_backend"); + config.remove("native_display_enabled"); + config.remove("vnc_enabled"); + config.remove("vnc_host"); + config.remove("vnc_port"); + config.remove("vnc_password_auth"); + config.remove("vnc_password"); + } + + /** + * Folds the five flat geometry keys onto the screens that have somewhere to put them. + * + *

The geometry was VM-level because the display was: one size, one refresh rate, one DPI, + * whichever device was producing the picture. With two devices that is one number answering + * two questions -- a 1400x1050 virtio-gpu mode and a 1280x720 framebuffer are an ordinary + * pair, and the old schema could not say it. Both screens inherit the old values, so a VM + * that is migrated and not touched comes up exactly as it did.

+ * + *

What does not carry over is what the flat keys could not hold. simplefb gets a poll rate, + * written out at the rate the bridge used for as long as it was not settable; the refresh rate + * and DPI go only to the GPU screen, because simplefb's geometry is fixed by the device tree + * and the device tree describes neither.

+ * + *

Gated on the legacy keys still being there rather than on the new ones being absent: the + * new ones have defaults equal to the old ones, so "absent" is indistinguishable from "folded + * and left at the default", and gating on that would re-run the fold over an edited screen. + * Dropping the keys is what makes this run once, for the same reason the bindings fold drops + * its own -- a second answer on disk for the same question is worse than no answer.

+ */ + private static void migrateGeometry(@NonNull DataItem config) { + var legacy = false; + for (var key : LEGACY_GEOMETRY) + if (config.opt(key, (DataItem) null) != null) { + legacy = true; + break; + } + if (!legacy) return; + + var width = config.optLong("display_width", DEFAULT_WIDTH); + var height = config.optLong("display_height", DEFAULT_HEIGHT); + + var gpu0 = of(config, ID_GPU0); + gpu0.setWidth(width); + gpu0.setHeight(height); + gpu0.setRefreshRate(config.optLong("display_refresh_rate", DEFAULT_REFRESH_RATE)); + gpu0.setDpiH(config.optLong("display_dpi_h", DEFAULT_DPI)); + gpu0.setDpiV(config.optLong("display_dpi_v", DEFAULT_DPI)); + + var fb = of(config, ID_SIMPLEFB); + fb.setWidth(width); + fb.setHeight(height); + // Written rather than left to the getter's default: this screen gaining a rate of its own + // is the visible half of the fold, and a value nobody can read out of the file is one the + // user cannot be shown having inherited. + fb.setPollHz(DEFAULT_POLL_HZ); + + for (var key : LEGACY_GEOMETRY) config.remove(key); + } + + /** + * Folds the VM-level {@code gpu_enabled} away: the gpu-0 switch is the virtio-gpu device now, + * and the renderer is a setting inside it. + * + *

The old pair was two answers to one question and they were allowed to disagree, so the + * fold has to decide what each disagreement meant.

+ * + *

Accelerator on, screen off. This was supposed to be a GPU that renders and scans + * out nothing, and it is the shape an acceleration test gets saved in. It never worked -- no + * desktop ever came up on such a VM -- so there is nothing to preserve: the device is off. A + * VM that was in that state was not doing anything, and saying so is more honest than carrying + * a configuration forward on the grounds that it was written down.

+ * + *

Accelerator off, screen on. The reverse disagreement was a real, ordinary VM in + * intent -- a display with no 3D -- and the old builder emitted no {@code --gpu} at all for + * it, so it had no display device either. The new model can say exactly what was meant, so it + * does: the device is on with the 2D renderer, which is also what repairs the VM.

+ * + *

Runs for any config from before the screens split, key or no key: an absent + * {@code gpu_enabled} read as false is exactly the second case, and gating on the key alone + * would leave those configs with a virtio-gpu screen and no renderer named.

+ */ + private static void migrateGpuDevice(@NonNull DataItem config, boolean preScreens) { + if (!preScreens && config.opt(LEGACY_GPU_ENABLED, (DataItem) null) == null) return; + var accelerated = config.optBoolean(LEGACY_GPU_ENABLED, false); + var gpu0 = find(config, ID_GPU0); + if (!accelerated && gpu0 != null && gpu0.isEnabled()) + // Whatever gpu_backend said was inert while the accelerator was off -- the old builder + // never read it -- so this is naming the renderer the VM actually ran, not losing one. + config.set("gpu_backend", GpuBackend.GPU_2D); + config.remove(LEGACY_GPU_ENABLED); + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/console/VMConsoleRouter.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/console/VMConsoleRouter.java index 8ba60ac98..d3f33d86f 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/console/VMConsoleRouter.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/console/VMConsoleRouter.java @@ -14,18 +14,24 @@ import java.util.UUID; import cn.classfun.droidvm.lib.daemon.DaemonConnection; +import cn.classfun.droidvm.lib.store.base.DataItem; +import cn.classfun.droidvm.lib.store.vm.DisplayExporter; import cn.classfun.droidvm.lib.store.vm.VMConfig; +import cn.classfun.droidvm.lib.store.vm.VMScreenConfig; import cn.classfun.droidvm.ui.vm.display.nativedisplay.display.VMNativeDisplayActivity; import cn.classfun.droidvm.ui.vm.display.vnc.base.BaseVncActivity; import cn.classfun.droidvm.ui.vm.display.vnc.display.VMVncDisplayActivity; -import cn.classfun.droidvm.ui.vm.display.vnc.display.VMVncPresentationActivity; /** * Shared "open the VM's default view" routing, so the VM-info screen (a console * button) and the VM-list auto-open-after-start pick the same thing: - * native display, else VNC, else the serial console (uart, then stdio). Keeping + * the first bound screen, else the serial console (uart, then stdio). Keeping * this in one place is why both paths agree instead of the list always opening * the UART console regardless of the VM's display. + * + *

Every display view is opened for one screen, named explicitly, because the VM can have two + * and they can be exported differently. The default picks the first one bound -- the only one a + * single-screen VM has; the chooser is where a two-screen VM is asked which.

*/ public final class VMConsoleRouter { private VMConsoleRouter() { @@ -37,13 +43,13 @@ private VMConsoleRouter() { */ public static void openDefault(@NonNull Context ctx, @NonNull UUID vmId, @NonNull VMConfig config, boolean running) { - var item = config.item; - if (item.optBoolean("native_display_enabled", false)) { - openNative(ctx, vmId, config); - return; - } - if (item.optBoolean("vnc_enabled", false)) { - openVnc(ctx, vmId, config); + var bound = VMScreenConfig.boundOf(config.item); + if (!bound.isEmpty()) { + var screen = bound.get(0); + if (screen.getExporter() == DisplayExporter.NATIVE) + openNative(ctx, vmId, config, screen.id); + else + openVnc(ctx, vmId, config, screen.id); return; } // Serial console: ask the daemon which streams exist. Prefer a real guest serial port @@ -88,27 +94,51 @@ public static void openConsole(@NonNull Context ctx, @NonNull UUID vmId, ctx.startActivity(intent); } - public static void openNative(@NonNull Context ctx, @NonNull UUID vmId, @NonNull VMConfig config) { + /** + * Opens [screenId]'s native display. The screen id travels in the intent rather than being + * inferred here, because the display service name is derived from it and a wrong guess is a + * console that waits forever on a binder nobody registers. + */ + public static void openNative(@NonNull Context ctx, @NonNull UUID vmId, + @NonNull VMConfig config, @NonNull String screenId) { var item = config.item; var intent = new Intent(ctx, VMNativeDisplayActivity.class); intent.putExtra(VMNativeDisplayActivity.EXTRA_VM_ID, vmId.toString()); intent.putExtra(VMNativeDisplayActivity.EXTRA_VM_NAME, config.getName()); - intent.putExtra(VMNativeDisplayActivity.EXTRA_WIDTH, item.optLong("display_width", 1280)); - intent.putExtra(VMNativeDisplayActivity.EXTRA_HEIGHT, item.optLong("display_height", 720)); + intent.putExtra(VMNativeDisplayActivity.EXTRA_SCREEN, screenId); + intent.putExtra(VMNativeDisplayActivity.EXTRA_INPUT_ENABLED, inputEnabled(item, screenId)); + // The screen's own size, not the VM's: the two screens have different geometry now, and + // the console is showing exactly one of them. + var screen = VMScreenConfig.find(item, screenId); + intent.putExtra(VMNativeDisplayActivity.EXTRA_WIDTH, + screen != null ? screen.getWidth() : VMScreenConfig.DEFAULT_WIDTH); + intent.putExtra(VMNativeDisplayActivity.EXTRA_HEIGHT, + screen != null ? screen.getHeight() : VMScreenConfig.DEFAULT_HEIGHT); ctx.startActivity(intent); } - public static void openVnc(@NonNull Context ctx, @NonNull UUID vmId, @NonNull VMConfig config) { - var intent = new Intent(ctx, VMVncDisplayActivity.class); - intent.putExtra(BaseVncActivity.EXTRA_VM_ID, vmId.toString()); - intent.putExtra(BaseVncActivity.EXTRA_VM_NAME, config.getName()); - ctx.startActivity(intent); + /** + * Whether [screenId] was started with its own absolute input devices. + * + *

This is what the config says now, which is only the same as what the running VM has if + * nobody edited it since. The console uses it to explain dead touch input, never to decide + * where to send events -- that answer comes from the daemon, which knows which sockets it + * actually bound.

+ */ + private static boolean inputEnabled(@NonNull DataItem item, @NonNull String screenId) { + var screen = VMScreenConfig.find(item, screenId); + return screen == null || screen.isInputEnabled(); } - public static void openVncExt(@NonNull Context ctx, @NonNull UUID vmId, @NonNull VMConfig config) { - var intent = new Intent(ctx, VMVncPresentationActivity.class); + public static void openVnc(@NonNull Context ctx, @NonNull UUID vmId, + @NonNull VMConfig config, @NonNull String screenId) { + var intent = new Intent(ctx, VMVncDisplayActivity.class); intent.putExtra(BaseVncActivity.EXTRA_VM_ID, vmId.toString()); intent.putExtra(BaseVncActivity.EXTRA_VM_NAME, config.getName()); + intent.putExtra(BaseVncActivity.EXTRA_SCREEN, screenId); + intent.putExtra(BaseVncActivity.EXTRA_INPUT_ENABLED, inputEnabled(config.item, screenId)); ctx.startActivity(intent); } + // openVncExt retired: projecting to an external display is now an action inside the VNC + // console's own menu (VMVncDisplayActivity#projectToExternalDisplay), not a chooser entry. } diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/display/VMNativeDisplayActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/display/VMNativeDisplayActivity.java index eb137a773..b83398d01 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/display/VMNativeDisplayActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/display/VMNativeDisplayActivity.java @@ -30,6 +30,7 @@ import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.TextView; +import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -53,6 +54,7 @@ import cn.classfun.droidvm.lib.daemon.DaemonConnection; import cn.classfun.droidvm.lib.daemon.ForegroundCallback; import cn.classfun.droidvm.lib.store.vm.NativeDisplay; +import cn.classfun.droidvm.lib.store.vm.VMScreenConfig; import cn.classfun.droidvm.lib.ui.DragTouchListener; import cn.classfun.droidvm.lib.ui.ImeInsetsExempt; import cn.classfun.droidvm.lib.ui.MaterialMenu; @@ -94,6 +96,17 @@ public final class VMNativeDisplayActivity extends AppCompatActivity private static final String TAG = "VMNativeDisplay"; public static final String EXTRA_VM_NAME = "vm_name"; public static final String EXTRA_VM_ID = "vm_id"; + /** + * Which screen this console shows. The display service name is built from it, so it has to + * come from whoever opened the console -- a VM can have two screens and only one of them is + * registered under the name this activity waits on. + */ + public static final String EXTRA_SCREEN = "screen"; + /** + * Whether that screen was configured with its own absolute input devices. Used only to say + * why touch is doing nothing; where the events go is the daemon's answer, not this one. + */ + public static final String EXTRA_INPUT_ENABLED = "input_enabled"; public static final String EXTRA_WIDTH = "display_width"; public static final String EXTRA_HEIGHT = "display_height"; @@ -132,6 +145,14 @@ public final class VMNativeDisplayActivity extends AppCompatActivity private String vmName = ""; private String vmId = ""; private String vmKey = ""; + /** + * The screen this console shows. It picks the display service to wait on, and it picks which + * screen's absolute input devices the touches land on -- the two devices are per screen, so + * "the VM's touchscreen" is not a thing that can be addressed any more. + */ + private String screenId = VMScreenConfig.ID_GPU0; + /** Whether that screen has absolute input devices at all; see {@link #EXTRA_INPUT_ENABLED}. */ + private boolean screenInputEnabled = true; private int guestWidth = 1280; private int guestHeight = 720; @@ -255,9 +276,12 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { var intent = getIntent(); vmName = orEmpty(intent.getStringExtra(EXTRA_VM_NAME)); vmId = orEmpty(intent.getStringExtra(EXTRA_VM_ID)); + screenId = orEmpty(intent.getStringExtra(EXTRA_SCREEN)); + if (screenId.isEmpty()) screenId = VMScreenConfig.ID_GPU0; + screenInputEnabled = intent.getBooleanExtra(EXTRA_INPUT_ENABLED, true); guestWidth = (int) intent.getLongExtra(EXTRA_WIDTH, 1280); guestHeight = (int) intent.getLongExtra(EXTRA_HEIGHT, 720); - vmKey = NativeDisplay.serviceNameFromId(vmId); + vmKey = NativeDisplay.serviceNameFromId(vmId, screenId); bindViews(); toolbar.setTitle(vmName.isEmpty() ? getString(R.string.native_display_title) : vmName); @@ -290,6 +314,7 @@ public void onLost() { private void onRootConnected(@NonNull INativeDisplayRootService service) { // Try a direct unix-socket sink to the daemon (one write per evdev frame, no IPC // round-trip); on any failure it falls back to the vm_input JSON-RPC path below. + directSink = new DirectInputSink(vmId, () -> screenId, service, this::sendInputToDaemon); inputForwarder = new InputForwarder(directSink); if (nativeExtraKeys != null) nativeExtraKeys.setForwarder(inputForwarder); // A forwarder is built fresh on every attach and starts in TOUCH, so it has to be told @@ -700,6 +725,7 @@ private boolean sendInputToDaemon(int channel, @NonNull byte[] data) { var req = new JSONObject(); req.put("command", "vm_input"); req.put("vm_id", vmId); + req.put("screen", screenId); req.put("channel", channel); req.put("data", Base64.encodeToString(data, Base64.NO_WRAP)); var resp = DaemonConnection.getInstance().request(req); @@ -948,6 +974,16 @@ private boolean onMenuItemClicked(@NonNull MenuItem item) { // translator to the matching virtio-input device. The segmented header shows the active mode. private void setInputModeTo(@NonNull InputMode mode) { if (inputMode == mode) return; + // Both absolute modes ride this screen's own devices, so with them switched off the mode + // is selectable and inert. Say so once, here, rather than leaving the user tapping a + // screen that answers nothing -- and say the true reason, which is a VM that has to be + // started again, not a setting that would take effect if they waited. + // + // The switch reaches typing too now, since the keyboard became this screen's rather than + // the VM's. The hint names no device, so it stays true of all three; MOUSE is still the + // one thing the switch does not touch, the relative pointer being the VM's. + if (!screenInputEnabled && mode != InputMode.MOUSE) + Toast.makeText(this, R.string.display_input_disabled_hint, Toast.LENGTH_LONG).show(); inputMode = mode; getSharedPreferences(INPUT_PREFS, MODE_PRIVATE).edit() .putInt(KEY_INPUT_MODE, inputMode.ordinal()).apply(); diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/DirectInputSink.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/DirectInputSink.java index f03f98155..a7be181e9 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/DirectInputSink.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/nativedisplay/input/DirectInputSink.java @@ -11,6 +11,8 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import java.util.function.Supplier; + import cn.classfun.droidvm.display.INativeDisplayRootService; /** @@ -20,11 +22,17 @@ * {@code connectto} that su-domain socket nor receive its fd over binder, but it can hand the bytes * across binder. Falls back to {@code fallback} on any failure so the feature degrades to the * original IPC path instead of dropping input. + * + *

Every write carries the screen the owning console is showing, because the absolute devices + * are per screen. The screen is read from the console rather than copied in at construction: a + * VNC console opened without one named learns which screen it got only when the daemon answers + * {@code vm_vnc_info}, and that answer can arrive after the broker binder does.

*/ public final class DirectInputSink implements InputForwarder.InputSink { private static final String TAG = "DirectInputSink"; private final String vmId; + private final Supplier screenId; private final INativeDisplayRootService rootService; private final InputForwarder.InputSink fallback; private volatile boolean closed = false; @@ -34,12 +42,19 @@ public final class DirectInputSink implements InputForwarder.InputSink { /** * @param vmId the VM id, used by the daemon to find the running VM's input channel. + * @param screenId the screen this console is currently showing; picks between two screens' + * absolute devices and their keyboards, and is ignored by the VM-wide + * relative pointer alone. Sent on every channel, so which channels are per + * screen stays {@link cn.classfun.droidvm.lib.store.vm.NativeDisplay}'s + * decision rather than something this side has to be taught. * @param rootService daemon broker binder that writes the bytes; null disables the direct path. * @param fallback sink used when the direct path is unavailable or errors. */ - public DirectInputSink(@NonNull String vmId, @Nullable INativeDisplayRootService rootService, + public DirectInputSink(@NonNull String vmId, @NonNull Supplier screenId, + @Nullable INativeDisplayRootService rootService, @Nullable InputForwarder.InputSink fallback) { this.vmId = vmId; + this.screenId = screenId; this.rootService = rootService; this.fallback = fallback; } @@ -49,7 +64,8 @@ public boolean write(int channel, @NonNull byte[] data) { if (closed || channel < 0 || channel >= CHANNEL_COUNT) return false; if (rootService != null) { try { - if (rootService.writeInput(vmId, channel, data)) { + var screen = screenId.get(); + if (rootService.writeInput(vmId, screen == null ? "" : screen, channel, data)) { lastWriteDirect = true; return true; } diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/base/BaseVncActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/base/BaseVncActivity.java index 158f353bc..533edd212 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/base/BaseVncActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/base/BaseVncActivity.java @@ -17,8 +17,6 @@ import android.annotation.SuppressLint; import android.content.ActivityNotFoundException; -import android.content.ClipData; -import android.content.ClipboardManager; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.ActivityInfo; @@ -34,6 +32,7 @@ import android.view.View; import android.view.WindowManager; import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; @@ -50,6 +49,7 @@ import cn.classfun.droidvm.R; import cn.classfun.droidvm.lib.daemon.DaemonConnection; import cn.classfun.droidvm.lib.perf.GamePerfHint; +import cn.classfun.droidvm.lib.ui.CopyableField; import cn.classfun.droidvm.lib.ui.ImeInsetsExempt; import cn.classfun.droidvm.ui.vm.display.base.DisplayExtraKeysPanel; import cn.classfun.droidvm.ui.vm.display.vnc.input.VncExtraKeysPanel; @@ -58,6 +58,17 @@ public abstract class BaseVncActivity extends AppCompatActivity implements ImeIn protected final String TAG = getClass().getSimpleName(); public static final String EXTRA_VM_NAME = "vm_name"; public static final String EXTRA_VM_ID = "vm_id"; + /** + * Which screen's VNC server to connect to. A VM can run one per screen on different ports, + * so the daemon is asked for that screen's settings rather than for "the VM's VNC". + */ + public static final String EXTRA_SCREEN = "screen"; + /** + * Whether that screen was configured with its own absolute input devices. Used only to say + * why an input mode is doing nothing; where the events go is the daemon's answer, not this + * one. Modes that ride RFB (the tablet pointer, the keyboard) are unaffected either way. + */ + public static final String EXTRA_INPUT_ENABLED = "input_enabled"; protected static final int DEFAULT_PORT = 5900; private static final int MAX_RECONNECT_ATTEMPTS = 5; private static final long RECONNECT_DELAY_MS = 2000; @@ -74,6 +85,10 @@ public abstract class BaseVncActivity extends AppCompatActivity implements ImeIn protected VncExtraKeysPanel vncExtraKeys; protected String vmName = ""; protected String vmId = ""; + /** Screen whose VNC server this view shows; empty means "the VM's first bound one". */ + protected String screenId = ""; + /** Whether that screen has absolute input devices at all; see {@link #EXTRA_INPUT_ENABLED}. */ + protected boolean screenInputEnabled = true; protected String vncHost = "127.0.0.1"; // Phone LAN address the daemon resolved for an IPv4-wildcard bind (offload // proxy IPs already excluded); empty when not applicable. Preferred over @@ -146,6 +161,9 @@ protected void onCreate(Bundle savedInstanceState) { if (vmName == null) vmName = ""; vmId = intent.getStringExtra(EXTRA_VM_ID); if (vmId == null) vmId = ""; + screenId = intent.getStringExtra(EXTRA_SCREEN); + if (screenId == null) screenId = ""; + screenInputEnabled = intent.getBooleanExtra(EXTRA_INPUT_ENABLED, true); bindViews(); setupToolbar(); // Before onSetupActivity() so subclasses can wire views (e.g. the physical keyboard) @@ -244,6 +262,12 @@ protected void fetchVncInfoAndConnect() { }); }; DaemonConnection.OnResponse res = resp -> { + // Adopt the screen the daemon resolved. Asking for "the VM's VNC" is answered with a + // particular screen's server, and input has to agree with that answer: the absolute + // devices are per screen, so a console still holding "" would have nowhere to send a + // touch even though it is showing a screen that has one. + var resolved = resp.optString("screen", ""); + if (!resolved.isEmpty()) screenId = resolved; vncHost = resp.optString("host", "127.0.0.1"); vncRemoteHost = resp.optString("remote_host", ""); vncPort = resp.optInt("port", DEFAULT_PORT); @@ -253,6 +277,7 @@ protected void fetchVncInfoAndConnect() { }; DaemonConnection.getInstance().buildRequest("vm_vnc_info") .put("vm_id", vmId) + .put("screen", screenId) .onResponse(res) .onUnsuccessful(f) .onError(err) @@ -667,23 +692,42 @@ protected String generateVncUri(boolean local) { return sb.toString(); } + /** + * The one connection dialog: what to point a viewer at, and the handoff to one. + * + *

It shows the {@code vnc://} URI for this device and, when the server is bound wider than + * loopback, the one another machine on the network would use -- the pair the separate "view + * URL" dialog used to show, which is why there is no longer a separate dialog: a bare + * {@code host:port} said nothing the URI does not already say. Connect hands the network URI + * to whatever app claims {@code vnc://}; the password rides in it as a query parameter, and + * the copy button is there for the viewers that ignore it.

+ */ protected void openWithExternalApp() { - var url = generateVncUri(false); - var host = resolveVncHost(false); - var target = fmt("%s:%d", host, vncPort); + var localUrl = generateVncUri(true); + var remoteUrl = generateVncUri(false); + boolean sameUrl = localUrl.equals(remoteUrl); boolean hasPassword = vncPassword != null && !vncPassword.isEmpty(); var view = getLayoutInflater().inflate(R.layout.dialog_vnc_external, null); - TextView etTarget = view.findViewById(R.id.et_target); + EditText etLocal = view.findViewById(R.id.et_local); + EditText etRemote = view.findViewById(R.id.et_remote); TextView etPassword = view.findViewById(R.id.et_password); + TextInputLayout tilRemote = view.findViewById(R.id.til_remote); TextInputLayout tilPassword = view.findViewById(R.id.til_password); - etTarget.setText(target); + etLocal.setText(localUrl); + CopyableField.setupReadOnly(etLocal, getString(R.string.vnc_external_hint_local)); + if (sameUrl) { + tilRemote.setVisibility(GONE); + } else { + etRemote.setText(remoteUrl); + CopyableField.setupReadOnly(etRemote, getString(R.string.vnc_external_hint_remote)); + } if (hasPassword) { etPassword.setText(vncPassword); } else { tilPassword.setVisibility(GONE); } DialogInterface.OnClickListener onConnect = (d, w) -> { - var intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + var intent = new Intent(Intent.ACTION_VIEW, Uri.parse(remoteUrl)); try { startActivity(intent); } catch (ActivityNotFoundException e) { @@ -698,41 +742,8 @@ protected void openWithExternalApp() { if (hasPassword) builder.setNeutralButton(R.string.vnc_external_copy_password, null); var dialog = builder.show(); - if (hasPassword) dialog.getButton(BUTTON_NEUTRAL).setOnClickListener(v -> { - var cm = getSystemService(ClipboardManager.class); - if (cm == null) return; - cm.setPrimaryClip(ClipData.newPlainText("VNC Password", vncPassword)); - Toast.makeText(this, R.string.vnc_menu_url_copied, Toast.LENGTH_SHORT).show(); - }); - } - - protected void showVncUrl() { - var local = generateVncUri(true); - var remote = generateVncUri(false); - boolean sameUrl = local.equals(remote); - boolean hasPassword = vncPassword != null && !vncPassword.isEmpty(); - var view = getLayoutInflater().inflate(R.layout.dialog_vnc_url, null); - TextView etLocal = view.findViewById(R.id.et_local); - TextView etRemote = view.findViewById(R.id.et_remote); - TextView etPassword = view.findViewById(R.id.et_password); - TextInputLayout tilPassword = view.findViewById(R.id.til_password); - TextInputLayout tilRemote = view.findViewById(R.id.til_remote); - etLocal.setText(local); - if (sameUrl) { - tilRemote.setVisibility(GONE); - } else { - etRemote.setText(remote); - } - if (hasPassword) { - etPassword.setText(vncPassword); - } else { - tilPassword.setVisibility(GONE); - } - new MaterialAlertDialogBuilder(this) - .setTitle(R.string.vnc_menu_view_url) - .setView(view) - .setPositiveButton(android.R.string.ok, null) - .show(); + if (hasPassword) dialog.getButton(BUTTON_NEUTRAL).setOnClickListener(v -> + CopyableField.copy(this, vncPassword, getString(R.string.vnc_external_hint_password))); } protected void reconnect() { @@ -774,9 +785,6 @@ protected boolean onMenuItemClicked(@NonNull MenuItem item) { } else if (id == R.id.menu_external) { openWithExternalApp(); return true; - } else if (id == R.id.menu_view_url) { - showVncUrl(); - return true; } return false; } diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/display/VMVncDisplayActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/display/VMVncDisplayActivity.java index ff39f1f1c..bfc4c21b5 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/display/VMVncDisplayActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/display/vnc/display/VMVncDisplayActivity.java @@ -33,6 +33,7 @@ import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; +import android.widget.Toast; import androidx.annotation.NonNull; import androidx.core.graphics.Insets; @@ -91,6 +92,11 @@ public final class VMVncDisplayActivity extends BaseVncActivity { // Per-mode input routing: whatever the VNC channel natively has goes over RFB (TABLET's // absolute pointer + the keyboard); the rest goes to the crosvm --input evdev devices via the // daemon (MOUSE = relative motion the guest renders a cursor for, TOUCH = raw multi-touch). + // What the RFB half lands on is this screen's own tablet and keyboard, built by crosvm behind + // this binding's VNC server -- so this console is an ordinary RFB client for those two, no + // different from TigerVNC on the same port, and the coordinate is read against this screen's + // geometry rather than some VM-wide pointer's. Both are absent when the screen's input switch + // is off (view-only); see setInputMode. // Seeded from the shared pref in onSetupActivity(); TOUCH only until that read. private InputMode inputMode = InputMode.TOUCH; private SharedPreferences prefs; @@ -125,6 +131,7 @@ public final class VMVncDisplayActivity extends BaseVncActivity { if (operationLabel != null) operationLabel.setVisibility(GONE); }; + // Unified MOUSE/TABLET gestures. TABLET lands on the RFB channel (this binding's own // absolute-tablet pointer); MOUSE lands on the crosvm relative-mouse device via vm_input. private final PointerGestureTranslator.Listener gestureListener = new PointerGestureTranslator.Listener() { @@ -222,6 +229,7 @@ private boolean sendInputToDaemon(int channel, @NonNull byte[] data) { var req = new JSONObject(); req.put("command", "vm_input"); req.put("vm_id", vmId); + req.put("screen", screenId); req.put("channel", channel); req.put("data", android.util.Base64.encodeToString(data, android.util.Base64.NO_WRAP)); var resp = DaemonConnection.getInstance().request(req); @@ -328,6 +336,8 @@ public void onCloseKeyboard() { new DaemonDisplayAttach.Listener() { @Override public void onAttached(@NonNull INativeDisplayRootService service) { + directSink = new DirectInputSink(vmId, () -> screenId, service, + VMVncDisplayActivity.this::sendInputToDaemon); } @Override @@ -617,6 +627,16 @@ private void applyInputMode() { private void setInputMode(InputMode mode) { if (inputMode == mode) return; + // Both absolute modes are inert with the switch off, the same as on the native console. + // TOUCH because this screen's multi-touch device is not created; TABLET because the switch + // is what the daemon sends as view-only=true, and a view-only binding has no tablet and no + // keyboard behind it -- crosvm drops the RFB pointer and key events rather than injecting + // them. So RFB is no longer a way around this screen's switch, which it was while those + // devices belonged to the VM instead of to the binding. MOUSE is: the relative pointer has + // no output binding and is not a screen's to switch off. Say why once, here, rather than + // leaving the user tapping at nothing. + if (!screenInputEnabled && mode != InputMode.MOUSE) + Toast.makeText(this, R.string.display_input_disabled_hint, Toast.LENGTH_LONG).show(); inputMode = mode; prefs.edit().putInt(KEY_INPUT_MODE, mode.ordinal()).apply(); viewport.resetToFit(); @@ -695,8 +715,23 @@ protected boolean onMenuItemClicked(@NonNull MenuItem item) { if (id == R.id.menu_fullscreen) { toggleFullscreen(); return true; + } else if (id == R.id.menu_project_external) { + projectToExternalDisplay(); return true; } return super.onMenuItemClicked(item); } + + // Projecting this screen onto an external display is the presentation console, reached from + // in here rather than from the VM's console chooser: it is an action on the screen already + // open, not a separate console to pick. The presentation activity puts up the display picker + // itself and shows nothing until one is chosen, so "pick a display, then land on it" is its + // own flow -- this only hands it the same vm/screen this console is already bound to. + private void projectToExternalDisplay() { + var intent = new android.content.Intent(this, VMVncPresentationActivity.class); + intent.putExtra(EXTRA_VM_ID, vmId); + intent.putExtra(EXTRA_VM_NAME, vmName); + intent.putExtra(EXTRA_SCREEN, screenId); + startActivity(intent); + } } diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/graphics/ScreenBindingRow.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/graphics/ScreenBindingRow.java index d1bd7c30d..12d64b13b 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/graphics/ScreenBindingRow.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/graphics/ScreenBindingRow.java @@ -1,7 +1,561 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. +package cn.classfun.droidvm.ui.vm.edit.graphics; + +import static android.content.DialogInterface.BUTTON_POSITIVE; +import static android.view.View.GONE; +import static android.view.View.VISIBLE; +import static java.lang.Integer.parseInt; +import static cn.classfun.droidvm.lib.utils.StringUtils.generateRandomPassword; +import static cn.classfun.droidvm.lib.utils.StringUtils.getEditText; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.WindowManager; +import android.widget.AutoCompleteTextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; + +import com.google.android.material.button.MaterialButton; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.google.android.material.textfield.TextInputEditText; +import com.google.android.material.textfield.TextInputLayout; + +import java.util.ArrayList; +import java.util.List; + +import cn.classfun.droidvm.R; +import cn.classfun.droidvm.lib.ui.IconItemAdapter; +import cn.classfun.droidvm.lib.store.base.DataItem; +import cn.classfun.droidvm.lib.store.vm.DisplayExporter; +import cn.classfun.droidvm.lib.store.vm.DisplayTransportCap; +import cn.classfun.droidvm.lib.store.vm.VMScreenConfig; +import cn.classfun.droidvm.ui.widgets.row.ChooseRowWidget; +import cn.classfun.droidvm.ui.widgets.row.SwitchRowWidget; import cn.classfun.droidvm.ui.widgets.row.TextRowWidget; + +/** + * One screen's rows in the graphics tab: the switch that says the VM has the device, how big that + * screen is and how often it produces a picture, and who exports it over what. + * + *

The two screens have the same shape, so the wiring is written once here and the layout + * carries one block of ids per screen. The differences are both in "display settings" and both + * follow from what the device is: a virtio-gpu mode has a refresh rate and a DPI the guest is told + * about, and a framebuffer nobody announces frames for has a poll rate instead. Neither has the + * other's, so exactly one of the two is shown.

+ * + *

The enable switch is passed in rather than found inside the block, because the renderer + * section sits between it and everything else on the virtio-gpu side, and an {@code } has + * nowhere to put a section in the middle of itself.

+ */ +final class ScreenBindingRow { + private static final int VNC_PASSWORD_LENGTH = 8; + /** The size bounds the editor saves within; the menu and its dialog both honour them. */ + private static final int MIN_EDGE = ScreenResolutionOptions.MIN_EDGE; + private static final int MAX_EDGE = 8192; + /** A mode's rate is bounded by what a panel could show; the poll rate by what crosvm takes. */ + private static final int MAX_REFRESH_RATE = 400; + /** The rates offered before "custom". Both screens take all three. */ + private static final int[] RATE_OPTIONS = {30, 60, 120}; + + /** Screen id, as stored and as handed to crosvm's {@code screen=}. */ + final String screenId; + /** True for {@code gpu-0}: it has a mode, so it has a refresh rate and a DPI. */ + private final boolean gpuScreen; + private final boolean defaultEnabled; + private final DisplayExporter defaultExporter; + + private final SwitchRowWidget swEnabled; + private final View options; + private final TextInputLayout tilResolution; + private final AutoCompleteTextView ddResolution; + private final TextInputLayout tilRate; + private final AutoCompleteTextView ddRate; private final TextRowWidget rowWidthCpuFallback; + private final View dpiOptions; + private final TextInputEditText etDpiH; + private final TextInputEditText etDpiV; + private final ChooseRowWidget chooseExporter; + private final ChooseRowWidget chooseTransport; + private final SwitchRowWidget swInputEnabled; + private final View vncOptions; + private final TextInputLayout tilHost; + private final AutoCompleteTextView ddHost; + private final TextInputEditText etPort; + private final SwitchRowWidget swPasswordAuth; + private final View passwordOptions; + private final TextInputEditText etPassword; + + /** + * The geometry as picked, rather than as typed. + * + *

It lives here because the two fields that show it are menus now: what a menu carries is a + * label, and the label is written from these numbers rather than parsed back out of. What + * reaches the config is unchanged -- the same three numbers under the same keys -- so a config + * written before this row was a menu still loads into it.

+ */ + private int width = (int) VMScreenConfig.DEFAULT_WIDTH; + private int height = (int) VMScreenConfig.DEFAULT_HEIGHT; + private int rate; + /** The sizes this device offers, settled once in {@link #init}. */ + @NonNull + private List sizes = List.of(); + /** + * The listen address as picked, rather than as typed -- the host field is a menu now, for the + * same reason the two above it are. + * + *

May be "", which is not the same as {@link VncHostOptions#WILDCARD} even though it does + * the same thing: a config that names no host is one this app has never written, and rewriting + * it on a save the user made for some other reason is not this row's business. So the empty + * string survives a load and a save untouched, and only the menu's own label resolves it -- to + * the wildcard, because that is what both backends do with a host left unset.

+ */ + @NonNull + private String vncHost = VMScreenConfig.NEW_VM_DEFAULT_VNC_HOST; + /** The phone's own addresses, once the daemon has answered; see {@link #setScannedHosts}. */ + @NonNull + private List scannedHosts = List.of(); + /** The entries the host menu currently offers, in the order it offers them. */ + @NonNull + private List hostOptions = List.of(); + + /** + * @param block the root of this screen's {@code partial_vm_screen_binding} include, and also + * the view whose visibility follows the switch. Every lookup below is scoped to + * it, which is what keeps the two includes' identical ids apart -- an + * activity-wide findViewById would always find the first. + * @param switch_ this screen's enable switch, which lives in the parent block. + */ + ScreenBindingRow(@NonNull String screenId, @NonNull View block, + @NonNull SwitchRowWidget switch_, + boolean defaultEnabled, @NonNull DisplayExporter defaultExporter) { + this.screenId = screenId; + this.gpuScreen = VMScreenConfig.ID_GPU0.equals(screenId); + this.defaultEnabled = defaultEnabled; + this.defaultExporter = defaultExporter; + swEnabled = switch_; + options = block; + tilResolution = block.findViewById(R.id.til_screen_resolution); + ddResolution = block.findViewById(R.id.dd_screen_resolution); + tilRate = block.findViewById(R.id.til_screen_rate); + ddRate = block.findViewById(R.id.dd_screen_rate); rowWidthCpuFallback = block.findViewById(R.id.row_screen_width_cpu_fallback); + dpiOptions = block.findViewById(R.id.screen_dpi_options); + etDpiH = block.findViewById(R.id.et_screen_dpi_h); + etDpiV = block.findViewById(R.id.et_screen_dpi_v); + chooseExporter = block.findViewById(R.id.choose_screen_exporter); + chooseTransport = block.findViewById(R.id.choose_screen_transport); + swInputEnabled = block.findViewById(R.id.sw_screen_input_enabled); + vncOptions = block.findViewById(R.id.screen_vnc_options); + tilHost = block.findViewById(R.id.til_screen_vnc_host); + ddHost = block.findViewById(R.id.dd_screen_vnc_host); + etPort = block.findViewById(R.id.et_screen_vnc_port); + swPasswordAuth = block.findViewById(R.id.sw_screen_vnc_password_auth); + passwordOptions = block.findViewById(R.id.screen_vnc_password_options); + etPassword = block.findViewById(R.id.et_screen_vnc_password); + MaterialButton btnClear = block.findViewById(R.id.btn_screen_vnc_password_clear); + MaterialButton btnGenerate = block.findViewById(R.id.btn_screen_vnc_password_generate); + btnClear.setOnClickListener(v -> etPassword.setText("")); + btnGenerate.setOnClickListener(v -> + etPassword.setText(generateRandomPassword(VNC_PASSWORD_LENGTH))); + // Which rate this screen has -- and whether it has a DPI at all -- is a property of the + // device, not of anything the user can change, so both are settled once here rather than + // in the visibility pass. One rate row either way; only its hint and its bounds differ. + rate = (int) (gpuScreen ? VMScreenConfig.DEFAULT_REFRESH_RATE : VMScreenConfig.NEW_VM_DEFAULT_POLL_HZ); + tilRate.setHint(block.getContext().getString(rateHint())); + dpiOptions.setVisibility(gpuScreen ? VISIBLE : GONE); + } + + /** + * Wires the listeners. {@code onChanged} runs after every change that another row can depend + * on -- the tab re-runs its whole visibility pass there rather than each row guessing what + * else moved. + */ + void init(@NonNull Runnable onChanged) { + // Off is a real choice: a screen the VM has but nobody is watching is a state crosvm + // accepts, not a half-configured one. The default is what a brand-new VM comes up with, + // since only edit mode ever calls load(). + chooseExporter.configure(DisplayExporter.class, defaultExporter); + applyTransportOptions(defaultExporter, null); + swEnabled.setChecked(defaultEnabled); + // The absolute devices are what a screen has unless the user says otherwise, so a new VM + // and a config written before the key both come up with them on. + swInputEnabled.setChecked(true); + swEnabled.setOnCheckedChangeListener(onChanged); + // The width decides whether the GPU copy can take this screen's frames at all, so picking + // a size is one of the changes another row depends on -- the only geometry one that is. + // Through the tab's whole pass like every other change, rather than poking the one row it + // moves. + bindSizeMenu(onChanged); + bindRateMenu(onChanged); + bindHostMenu(); + chooseExporter.setOnValueChangedListener(() -> { + // The ladder belongs to the edge, so changing who is on the far end of it changes + // which rungs exist -- not just which are reachable. + applyTransportOptions(getExporter(), null); + onChanged.run(); + }); + swPasswordAuth.setOnCheckedChangeListener(onChanged); + } + + /** + * The size menu: every size this device has a reason to offer, smallest first, then "custom". + * + *

Picking is the whole of the input path now, which is what turns the geometry check into a + * check on a stored value rather than on something half-typed: every entry above the last is + * in bounds by construction, and the last one validates before it hands anything back.

+ */ + private void bindSizeMenu(@NonNull Runnable onChanged) { + var ctx = ddResolution.getContext(); + var panel = panelSize(ctx); + sizes = ScreenResolutionOptions.build(panel[0], panel[1]); + var labels = new ArrayList(sizes.size() + 1); + for (var size : sizes) labels.add(sizeLabel(ctx, size.width, size.height)); + labels.add(ctx.getString(R.string.create_vm_display_custom)); + ddResolution.setAdapter(IconItemAdapter.create(ctx, labels, R.drawable.ic_monitor)); + ddResolution.setOnItemClickListener((parent, view, pos, id) -> { + if (pos < sizes.size()) { + var size = sizes.get(pos); + width = size.width; + height = size.height; + tilResolution.setError(null); + onChanged.run(); + } else { + askCustomSize(ctx, onChanged); + } + // The menu wrote the entry's own label into the field on its way out. Put the value + // back: after "custom" it would otherwise sit there reading "Custom..." while the + // dialog is still open, and after a pick the label is this row's to format. + applySizeText(); + }); + applySizeText(); + } + + /** The rate menu. Same shape as the size menu, and the same reason for it. */ + private void bindRateMenu(@NonNull Runnable onChanged) { + var ctx = ddRate.getContext(); + var labels = new ArrayList(RATE_OPTIONS.length + 1); + for (var hz : RATE_OPTIONS) labels.add(rateLabel(ctx, hz)); + labels.add(ctx.getString(R.string.create_vm_display_custom)); + ddRate.setAdapter(IconItemAdapter.create(ctx, labels, R.drawable.ic_speedometer)); + ddRate.setOnItemClickListener((parent, view, pos, id) -> { + if (pos < RATE_OPTIONS.length) { + rate = RATE_OPTIONS[pos]; + tilRate.setError(null); + onChanged.run(); + } else { + askCustomRate(ctx, onChanged); + } + applyRateText(); + }); + applyRateText(); + } + + /** + * The host menu: the two fixed addresses, the phone's own once they are known, then "custom". + * + *

Rebuilt rather than filtered when the scan lands, because the scan is what most of the + * list is. Nothing here calls {@code onChanged}: no other row depends on which address this + * screen listens on -- unlike the port, which the tab checks pairwise across screens.

+ */ + private void bindHostMenu() { + var ctx = ddHost.getContext(); + hostOptions = VncHostOptions.build(scannedHosts, displayHost()); + var labels = new ArrayList(hostOptions.size() + 1); + for (var option : hostOptions) labels.add(hostLabel(ctx, option)); + labels.add(ctx.getString(R.string.create_vm_display_custom)); + ddHost.setAdapter(IconItemAdapter.create(ctx, labels, R.drawable.ic_ip_network)); + ddHost.setOnItemClickListener((parent, view, pos, id) -> { + if (pos < hostOptions.size()) { + vncHost = hostOptions.get(pos).addr; + tilHost.setError(null); + } else { + askCustomHost(ctx); + } + // Same as the two menus above: the entry wrote its own label into the field on its way + // out, and after "custom" that label would sit there reading "Custom..." while the + // dialog is still open. + applyHostText(); + }); + applyHostText(); + } + + /** + * The phone's own addresses, from the daemon. Arrives after the row is built and possibly + * after a config has been loaded over it, so it rebuilds the menu and leaves the selection + * alone -- what was picked stays picked, and only gains the interface name beside it if the + * scan found the same address. + */ + void setScannedHosts(@NonNull List scanned) { + scannedHosts = scanned; + bindHostMenu(); + } + + /** + * The address the menu shows as selected: the stored one, or the wildcard when nothing is + * stored, which is what a host left unset already means to both backends. + */ + @NonNull + private String displayHost() { + return vncHost.isEmpty() ? VncHostOptions.WILDCARD : vncHost; + } + + /** How one entry reads: the address, and the interface it was found on when there is one. */ + @NonNull + private static String hostLabel(@NonNull Context ctx, @NonNull VncHostOptions.Option option) { + return option.ifname.isEmpty() ? option.addr + : ctx.getString(R.string.create_vm_vnc_host_iface_fmt, option.addr, option.ifname); + } + + private void applyHostText() { + var host = displayHost(); + var label = host; + for (var option : hostOptions) + if (option.addr.equals(host)) { + label = hostLabel(ddHost.getContext(), option); + break; + } + ddHost.setText(label, false); + } + + /** + * The host menu's last entry: one address, checked for what would break the option string it + * ends up inside rather than for being reachable. Accepting it adds it to the list, so the + * field shows an entry rather than a value the menu cannot highlight. + */ + private void askCustomHost(@NonNull Context ctx) { + var view = LayoutInflater.from(ctx).inflate(R.layout.dialog_vnc_host, null); + TextInputLayout til = view.findViewById(R.id.til_custom_vnc_host); + TextInputEditText etCustom = view.findViewById(R.id.et_custom_vnc_host); + etCustom.setText(displayHost()); + var dialog = new MaterialAlertDialogBuilder(ctx) + .setTitle(R.string.create_vm_vnc_host_custom_title) + .setView(view) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(android.R.string.ok, null) + .show(); + // Wired after show() for the same reason the size dialog is: a refused value has to stay + // on screen with its error, and the listener setPositiveButton takes dismisses regardless. + dialog.getButton(BUTTON_POSITIVE).setOnClickListener(v -> { + var host = getEditText(etCustom).trim(); + if (!VncHostOptions.isLiteral(host)) { + til.setError(ctx.getString(R.string.create_vm_error_invalid_host)); + return; + } + vncHost = host; + tilHost.setError(null); + bindHostMenu(); + dialog.dismiss(); + }); + } + + /** How a size reads, whether the menu offers it, a dialog produced it or a config carried it. */ + @NonNull + private static String sizeLabel(@NonNull Context ctx, int w, int h) { + return ctx.getString(R.string.create_vm_display_size_fmt, w, h); + } + + @NonNull + private static String rateLabel(@NonNull Context ctx, int hz) { + return ctx.getString(R.string.create_vm_display_rate_fmt, hz); + } + + private void applySizeText() { + ddResolution.setText(sizeLabel(ddResolution.getContext(), width, height), false); + } + + private void applyRateText() { + ddRate.setText(rateLabel(ddRate.getContext(), rate), false); + } + + /** The size menu's last entry: two numbers, checked against the bounds the editor saves in. */ + private void askCustomSize(@NonNull Context ctx, @NonNull Runnable onChanged) { + var view = LayoutInflater.from(ctx).inflate(R.layout.dialog_screen_resolution, null); + TextInputEditText etW = view.findViewById(R.id.et_custom_width); + TextInputEditText etH = view.findViewById(R.id.et_custom_height); + etW.setText(String.valueOf(width)); + etH.setText(String.valueOf(height)); + var dialog = new MaterialAlertDialogBuilder(ctx) + .setTitle(R.string.create_vm_display_custom_size_title) + .setView(view) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(android.R.string.ok, null) + .show(); + // Wired after show() so a refused value can stay on screen with its error: the listener + // setPositiveButton takes dismisses the dialog whatever it decides. + dialog.getButton(BUTTON_POSITIVE).setOnClickListener(v -> { + var w = bounded(etW, MIN_EDGE, MAX_EDGE); + var h = bounded(etH, MIN_EDGE, MAX_EDGE); + if (w == 0 || h == 0) return; + width = w; + height = h; + tilResolution.setError(null); + applySizeText(); + onChanged.run(); + dialog.dismiss(); + }); + } + + /** The rate menu's last entry, bounded by whichever rate this screen has. */ + private void askCustomRate(@NonNull Context ctx, @NonNull Runnable onChanged) { + var view = LayoutInflater.from(ctx).inflate(R.layout.dialog_screen_rate, null); + TextInputLayout til = view.findViewById(R.id.til_custom_rate); + TextInputEditText etRate = view.findViewById(R.id.et_custom_rate); + til.setHint(ctx.getString(rateHint())); + etRate.setText(String.valueOf(rate)); + var dialog = new MaterialAlertDialogBuilder(ctx) + .setTitle(rateHint()) + .setView(view) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(android.R.string.ok, null) + .show(); + dialog.getButton(BUTTON_POSITIVE).setOnClickListener(v -> { + var hz = bounded(etRate, rateMin(), rateMax()); + if (hz == 0) return; + rate = hz; + tilRate.setError(null); + applyRateText(); + onChanged.run(); + dialog.dismiss(); + }); + } + + /** + * The number typed into a dialog field, or 0 with the reason shown on the field -- the same + * reading, bounds and message the tab applies to every other number on this screen, since the + * value is bound for the same config. + */ + private static int bounded(@NonNull TextInputEditText field, int min, int max) { + field.setError(null); + try { + var value = parseInt(getEditText(field)); + if (value < min || value > max) throw new IllegalArgumentException(); + return value; + } catch (Exception ignored) { + field.setError(field.getContext().getString(R.string.create_vm_error_invalid_number)); + return 0; + } + } + + @StringRes + private int rateHint() { + return gpuScreen + ? R.string.create_vm_display_refresh_rate : R.string.create_vm_display_poll_hz; + } + + private int rateMin() { + return gpuScreen ? 1 : (int) VMScreenConfig.MIN_POLL_HZ; + } + + private int rateMax() { + return gpuScreen ? MAX_REFRESH_RATE : (int) VMScreenConfig.MAX_POLL_HZ; + } + + /** The panel this app is running on, in whatever rotation it is held, or 0x0 if it cannot be + * asked -- which offers the fixed sizes alone rather than inventing a device. */ + @NonNull + private static int[] panelSize(@NonNull Context ctx) { + var wm = ctx.getSystemService(WindowManager.class); + if (wm == null) return new int[]{0, 0}; + var bounds = wm.getMaximumWindowMetrics().getBounds(); + return new int[]{bounds.width(), bounds.height()}; + } + + /** + * Rebuilds the transport menu for [exporter] and settles on a value. + * + *

Three things decide the menu, and all of them are the edge's rather than this row's: the + * rungs this (screen, exporter) pair has at all, which of them this build can honour, and what + * the highest honourable one is. Rungs that exist but are not built yet are listed and refused + * with a note, so the ladder reads whole; rungs that cannot exist on this pair are absent, + * because offering a choice nobody will ever be able to make is worse than not naming it.

+ * + * @param want the stored ceiling to restore on load, if this edge still offers it; or null on + * a fresh row or an exporter switch, which re-evaluates to the edge's default + * instead of carrying the old pick across. Switching exporter is switching which + * ladder this is, and each ladder's fastest rung is a different one, so the pick + * follows the new edge rather than lingering on a value that was only best for the + * old one. A restore that the edge no longer offers also falls back to the default. + */ + private void applyTransportOptions(@NonNull DisplayExporter exporter, + @Nullable DisplayTransportCap want) { + // No exporter, no edge, no ladder. The row is hidden in that state; leave the menu as it + // was rather than emptying it, since the picker refuses an empty item list outright. + var options = DisplayTransportCap.optionsFor(screenId, exporter); + if (options.length == 0) return; + chooseTransport.setItems(options); + transportMenuBuilt = true; + chooseTransport.setDisabledItems( + chooseTransport.getContext().getString( + DisplayTransportCap.unimplementedFor(screenId, exporter)); + // want != null is a load: restore the stored ceiling if this edge still offers it. want == + // null is a fresh row or an exporter switch, and then the ceiling is re-evaluated to the + // new edge's default rather than carried over -- the fastest rung differs per exporter + // (GPU_HW on VNC, plain GPU on native), so carrying the old pick left a screen switched to + // VNC sitting at plain GPU copy when the hardware-encode rung is what it should default to. + var pick = want != null && DisplayTransportCap.isOfferedFor(screenId, exporter, want) + ? want : DisplayTransportCap.defaultFor(screenId, exporter); + chooseTransport.setSelectedItem(pick); + } + + /** Set by the first applyTransportOptions; the picker cannot be read before it. */ + private boolean transportMenuBuilt = false; + + /** The transport currently selected, or null before the menu has ever been built. */ + @Nullable + private DisplayTransportCap currentTransport() { + // Asking the widget before its first setItems is not an IllegalStateException, it is an + // NPE from deep inside (ChooseRowWidget.getSelectedItem -> getPicker() on nothing) -- + // which is exactly how the editor crashed on first open while every unit test, none of + // which inflates the real widget, stayed green. Track the state ourselves instead of + // classifying the widget's failure modes. + if (!transportMenuBuilt) return null; + return chooseTransport.getSelectedItem(); + } + + /** + * Whether the listen address this row holds is one the VM can be started with, said on the row + * that holds it. + * + *

Nothing the menu offers can fail this and the custom dialog checks before it returns, so + * what it catches is a config that arrived with something else in it -- hand-edited, or written + * back when the field was free text. Worth catching rather than passing on: crosvm refuses a + * host it cannot parse, so the VM would fail to start with the reason only in a log.

+ */ + boolean validateVncHost() { + tilHost.setError(null); + // Empty is the config that names no host, which is legal and means every address; the row + // shows it as the wildcard and writes it back unchanged. + if (vncHost.isEmpty() || VncHostOptions.isLiteral(vncHost)) return true; + tilHost.setError(tilHost.getContext().getString(R.string.create_vm_error_invalid_host)); + return false; + } + + boolean isScreenEnabled() { + return swEnabled.isChecked(); + } + + @NonNull + DisplayExporter getExporter() { + return chooseExporter.getSelectedItem(); + } + + void updateVisibility() { + var enabled = swEnabled.isChecked(); + var exporter = getExporter(); + options.setVisibility(enabled ? VISIBLE : GONE); + // No exporter, no edge for a transport to run along, so the row would be a ceiling on + // nothing. Its value is kept while hidden, like the VNC block's. + chooseTransport.setVisibility( + enabled && exporter != DisplayExporter.NONE ? VISIBLE : GONE); + vncOptions.setVisibility( + enabled && exporter == DisplayExporter.VNC ? VISIBLE : GONE); + passwordOptions.setVisibility(swPasswordAuth.isChecked() ? VISIBLE : GONE); // The one thing the transport ceiling cannot promise: a width whose stride the blit's // dma-buf import will not take settles a rung lower, silently, and the only clue is a line // in the console. Say so beside the field that causes it. Nothing is refused and nothing is @@ -9,8 +563,141 @@ var transport = currentTransport(); rowWidthCpuFallback.setVisibility( enabled && transport != null && DisplayTransportCap.cpuFallbackFromWidth( + screenId, exporter, transport, width) ? VISIBLE : GONE); } /** + * Whether the geometry this row holds is one the VM can be saved with, said on the row that + * holds it. + * + *

Nothing the two menus produce can fail this: every size and rate they offer is in bounds, + * and their dialogs check before they return. What it catches is a config that arrived out of + * bounds -- hand-edited, or written where the limits were different -- and it reports on a row + * that is on screen, because a save refused for a reason the screen does not show is a VM the + * user cannot fix.

*/ + boolean validateGeometry() { + tilResolution.setError(null); + tilRate.setError(null); + var ctx = tilResolution.getContext(); + if (width < MIN_EDGE || width > MAX_EDGE || height < MIN_EDGE || height > MAX_EDGE) { + tilResolution.setError(ctx.getString(R.string.create_vm_error_invalid_number)); + return false; + } + if (rate < rateMin() || rate > rateMax()) { + tilRate.setError(ctx.getString(R.string.create_vm_error_invalid_number)); + return false; + } + return true; + } + + void load(@NonNull DataItem config) { + var screen = VMScreenConfig.find(config, screenId); + if (screen == null) { + // No entry at all: the screen has never been configured, so the row answers with + // what a new one gets rather than with the sentinel for "watched by nobody". + swEnabled.setChecked(false); + chooseExporter.setSelectedItem(defaultExporter); + applyTransportOptions(defaultExporter, null); + return; + } + swEnabled.setChecked(screen.isEnabled()); + chooseExporter.setSelectedItem(screen.getExporter()); + applyTransportOptions(screen.getExporter(), screen.getTransportCap()); + // Absent in the stored config means on, so an existing VM keeps the devices it had + // without its file being rewritten to say so. + swInputEnabled.setChecked(screen.isInputEnabled()); + width = (int) screen.getWidth(); + height = (int) screen.getHeight(); + applySizeText(); + if (gpuScreen) { + rate = (int) screen.getRefreshRate(); + etDpiH.setText(String.valueOf(screen.getDpiH())); + etDpiV.setText(String.valueOf(screen.getDpiV())); + } else { + rate = (int) screen.getPollHz(); + } + applyRateText(); + // Straight through, empty included: see the field's own note. The menu is rebuilt because + // the entry list depends on what is selected -- a stored address the scan did not find is + // appended so it stays selectable. + vncHost = screen.getVncHost(); + bindHostMenu(); + var port = screen.getVncPort(); + etPort.setText(port > 0 ? String.valueOf(port) : ""); + swPasswordAuth.setChecked(screen.isVncPasswordAuth()); + etPassword.setText(screen.getVncPassword()); + } + + void save(@NonNull DataItem config) { + var screen = VMScreenConfig.of(config, screenId); + // The switch stores the switch, and nothing else. It used to also write NONE over the + // exporter of a screen it was turning off, which read back as a choice the next time the + // editor opened: turning the device on again showed it bound to nobody, with the pick the + // user had made gone. Every reader already asks whether a screen is on before asking what + // it is bound to -- directly (isInputBridgeNeeded, buildScreenExportersCommand) or through + // a filter that does (boundOf, hasAbsoluteInput) -- so "off" needs no second spelling in + // the fields underneath, the same way the geometry below has never needed one. + var exporter = getExporter(); + screen.setEnabled(swEnabled.isChecked()); + screen.setExporter(exporter); + // Written whatever the exporter is, so a rung picked under one exporter is still there + // after a detour through another -- and so a rung that is refused today is remembered for + // the build that can honour it. + var transport = currentTransport(); + if (transport != null) screen.setTransportCap(transport); + screen.setInputEnabled(swInputEnabled.isChecked()); + // The geometry is written whether or not the screen is on, for the same reason the VNC + // block below keeps its values: a size typed once and then switched off should still be + // there when the switch comes back. Only the rows this screen actually has are written -- + // a poll rate on the GPU screen would be a number nothing reads. + screen.setWidth(width); + screen.setHeight(height); + if (gpuScreen) { + screen.setRefreshRate(rate); + screen.setDpiH(parseInt(getEditText(etDpiH))); + screen.setDpiV(parseInt(getEditText(etDpiV))); + } else { + screen.setPollHz(rate); } + // The VNC block keeps its values even while hidden, so a screen switched to native and + // back finds its port and password where it left them. Only the auth switch is written + // from the live state, since it is what decides whether the password is used at all. + if (exporter == DisplayExporter.VNC) { + screen.setVncHost(vncHost); + var portStr = getEditText(etPort); + screen.setVncPort(portStr.isEmpty() ? -1 : parseInt(portStr)); + var auth = swPasswordAuth.isChecked(); + screen.setVncPasswordAuth(auth); + if (auth) screen.setVncPassword(getEditText(etPassword)); + } + } + + /** The port typed into this row, or -1 for "let the daemon pick one". */ + int typedVncPort() { + try { + return portStr.isEmpty() ? -1 : parseInt(portStr); + } catch (Exception ignored) { + return -1; + } + } + + @NonNull + TextInputEditText portField() { + return etPort; + } + + @NonNull + TextInputEditText dpiHField() { + return etDpiH; + } + + @NonNull + TextInputEditText dpiVField() { + return etDpiV; + } + + boolean isGpuScreen() { + return gpuScreen; + } +} diff --git a/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/graphics/ScreenResolutionOptions.java b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/graphics/ScreenResolutionOptions.java new file mode 100644 index 000000000..86836ccd9 --- /dev/null +++ b/app/src/main/java/cn/classfun/droidvm/ui/vm/edit/graphics/ScreenResolutionOptions.java @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright DroidVM contributors +// Additional permissions apply; see ADDITIONAL-PERMISSIONS in the repository root. +package cn.classfun.droidvm.ui.vm.edit.graphics; + +import androidx.annotation.NonNull; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * The sizes the resolution dropdown offers, and the only part of that dropdown that can be decided + * without a device. + * + *

Two of them are fixed ({@code 1280x720}, {@code 1920x1080}) and two are the phone's own panel + * -- whole, and halved on each axis -- because a guest that matches the panel is the one case where + * nothing has to scale, and half of it is the one that costs a quarter as much to draw. All four are + * landscape: a VM's screen is a desktop's screen, so the larger number is always the width, however + * the phone reports its own.

+ * + *

They are ordered by area rather than in the order they are named, since "smaller or larger + * than the one I have" is the only comparison between them a user can make at a glance, and the + * phone-derived pair lands in a different place on every device. A size that two rules produce + * appears once -- the list is of sizes, and where a size came from is not something it says.

+ */ +final class ScreenResolutionOptions { + /** The floor the geometry validator enforces; a rule that would produce less offers nothing. */ + static final int MIN_EDGE = 320; + + private static final int[][] PRESETS = {{1280, 720}, {1920, 1080}}; + + static final class Option { + final int width; + final int height; + + Option(int width, int height) { + this.width = width; + this.height = height; + } + + long area() { + return (long) width * height; + } + + boolean is(int w, int h) { + return width == w && height == h; + } + } + + private ScreenResolutionOptions() { + } + + /** + * The dropdown's sizes for a panel of {@code phoneWidth x phoneHeight}, smallest area first. + * + * @param phoneWidth the panel's width in pixels, in whatever rotation it was read; 0 or less + * when there is no panel to ask, which yields the fixed sizes alone rather + * than inventing a device. + * @param phoneHeight the panel's height, likewise. + */ + @NonNull + static List
diff --git a/app/src/main/res/menu/menu_vnc_presentation_menu.xml b/app/src/main/res/menu/menu_vnc_presentation_menu.xml index 78da6c1df..1a8c04893 100644 --- a/app/src/main/res/menu/menu_vnc_presentation_menu.xml +++ b/app/src/main/res/menu/menu_vnc_presentation_menu.xml @@ -16,9 +16,4 @@ android:icon="@drawable/ic_open_in" android:title="@string/vnc_menu_external_app" /> - - diff --git a/app/src/test/java/cn/classfun/droidvm/lib/store/vm/DisplayTransportCapTest.java b/app/src/test/java/cn/classfun/droidvm/lib/store/vm/DisplayTransportCapTest.java index e7987bb13..f0b42036b 100644 --- a/app/src/test/java/cn/classfun/droidvm/lib/store/vm/DisplayTransportCapTest.java +++ b/app/src/test/java/cn/classfun/droidvm/lib/store/vm/DisplayTransportCapTest.java @@ -1,8 +1,140 @@ +package cn.classfun.droidvm.lib.store.vm; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import cn.classfun.droidvm.lib.store.base.DataItem; + +/** + * The transport ladder: which rungs each (screen, exporter) edge has, which of them this build can + * honour, and what the app therefore puts on crosvm's command line. + * + *

Both ends decide the set, which is why every case here names both. The distinction the tests + * are really holding is between a rung that is unbuilt (offered, refused, and expected to + * arrive) and one that is unreachable (absent, because it never will).

* *

Two of the predicates below belong to the daemon and the editor rather than to this enum, and * are held here for the same reason: they decide whether a rung the ladder offers is actually * climbed at run time -- one from the VM's bindings, one from a number the user typed -- and * neither of their owners can be stood up without a device.

+ */ +public class DisplayTransportCapTest { + private static final String GPU0 = VMScreenConfig.ID_GPU0; + private static final String FB = VMScreenConfig.ID_SIMPLEFB; + + @Test + public void everyLadderIsBottomRungFirstAndStartsAtCpuCopy() { + // CPU copy needs nothing from either end, so it is the one rung that is always there -- + // and it is what makes a ceiling always satisfiable. + for (var screen : new String[]{GPU0, FB}) + for (var exporter : new DisplayExporter[]{DisplayExporter.NATIVE, DisplayExporter.VNC}) + assertEquals(DisplayTransportCap.CPU, + DisplayTransportCap.optionsFor(screen, exporter)[0]); + } + + @Test + public void theNativeDisplayCanBeLentARenderTargetOnlyByTheGpuScreen() { + // Zero copy is the sink lending a buffer for the guest's rendering to land in. simplefb's + // framebuffer is a fixed window of guest memory named in the device tree and no + // AHardwareBuffer can be made to wrap it, so that rung is not unbuilt there, it is + // unreachable -- absent from the menu rather than greyed in it. + assertArrayEquals( + new DisplayTransportCap[]{ + DisplayTransportCap.CPU, DisplayTransportCap.GPU, DisplayTransportCap.ZERO}, + DisplayTransportCap.optionsFor(GPU0, DisplayExporter.NATIVE)); + assertArrayEquals( + new DisplayTransportCap[]{DisplayTransportCap.CPU, DisplayTransportCap.GPU}, + DisplayTransportCap.optionsFor(FB, DisplayExporter.NATIVE)); + assertFalse(DisplayTransportCap.isOfferedFor( + FB, DisplayExporter.NATIVE, DisplayTransportCap.ZERO)); + } + + @Test + public void vncsTopRungIsAnEncoderRatherThanALentBuffer() { + // There is nowhere to lend a render target to a remote client, so VNC's rung above a blit + // is handing the result to a hardware encoder instead of an RFB rectangle. Same ladder on + // both screens, because it is the exporter's end that supplies it. + for (var screen : new String[]{GPU0, FB}) + assertArrayEquals( + new DisplayTransportCap[]{ + DisplayTransportCap.CPU, DisplayTransportCap.GPU, DisplayTransportCap.GPU_HW}, + DisplayTransportCap.optionsFor(screen, DisplayExporter.VNC)); + } + + @Test + public void anUnwatchedScreenHasNoLadderAtAll() { + // No exporter, no edge; a ceiling on nothing is not a smaller menu, it is no menu. + for (var screen : new String[]{GPU0, FB}) + assertEquals(0, DisplayTransportCap.optionsFor(screen, DisplayExporter.NONE).length); + } + + @Test + public void whatThisBuildRefusesIsListedRatherThanHidden() { + // Offered and greyed, so the ladder reads whole and a rung landing later is recognisably + // the thing that was already there. + assertArrayEquals(new DisplayTransportCap[]{DisplayTransportCap.ZERO}, + DisplayTransportCap.unimplementedFor(GPU0, DisplayExporter.NATIVE)); + assertEquals(0, + DisplayTransportCap.unimplementedFor(FB, DisplayExporter.NATIVE).length); + } + + @Test + public void theDefaultIsTheHighestRungThatActuallyWorks() { + // Not the highest offered: defaulting every VM to a ceiling nothing can satisfy is a + // promise the negotiation would quietly break. This one restricts nothing that works, and + assertEquals(DisplayTransportCap.GPU, + } + + @Test + public void theStoredTokenIsTheOneCrosvmTakes() { + // Written into the config and onto a command line, so one value must not have two + // spellings. Read back case-insensitively, because a hand-edited file is still a file. + assertEquals("cpu", DisplayTransportCap.CPU.getToken()); + assertEquals("gpu-hw", DisplayTransportCap.GPU_HW.getToken()); + for (var cap : DisplayTransportCap.values()) + assertEquals(cap, DisplayTransportCap.fromToken(cap.getToken())); + assertEquals(DisplayTransportCap.GPU_HW, DisplayTransportCap.fromToken("GPU-HW")); + assertNull(DisplayTransportCap.fromToken("")); + assertNull(DisplayTransportCap.fromToken(null)); + assertNull(DisplayTransportCap.fromToken("zero-copy")); + } + + @Test + public void aScreenWithNoStoredCeilingAnswersWithItsEdgeDefault() { + var item = DataItem.newObject(); + var fb = VMScreenConfig.of(item, VMScreenConfig.ID_SIMPLEFB); + fb.setEnabled(true); + fb.setExporter(DisplayExporter.VNC); + fb.setExporter(DisplayExporter.NATIVE); + assertEquals(DisplayTransportCap.GPU, fb.getTransportCap()); + } + + @Test + public void aCeilingTheEdgeDoesNotHaveFallsBackWithoutBeingErased() { + // Reading under a different exporter must not lose the answer: the user is allowed to look + // at the other exporter's settings and come back. + var item = DataItem.newObject(); + var gpu0 = VMScreenConfig.of(item, VMScreenConfig.ID_GPU0); + gpu0.setEnabled(true); + gpu0.setExporter(DisplayExporter.NATIVE); + gpu0.setTransportCap(DisplayTransportCap.ZERO); + assertEquals(DisplayTransportCap.ZERO, gpu0.getTransportCap()); + gpu0.setExporter(DisplayExporter.VNC); + gpu0.setExporter(DisplayExporter.NATIVE); + assertEquals(DisplayTransportCap.ZERO, gpu0.getTransportCap()); + } + + @Test + var item = DataItem.newObject(); + + } + + } @Test var item = DataItem.newObject(); @@ -51,3 +183,4 @@ public void aSimplefbWidthOffTheGrainSpendsTheGpuCopy() { assertFalse(DisplayTransportCap.cpuFallbackFromWidth( FB, DisplayExporter.NATIVE, DisplayTransportCap.GPU, 0)); } +} diff --git a/app/src/test/java/cn/classfun/droidvm/lib/store/vm/VMCreationDefaultsTest.java b/app/src/test/java/cn/classfun/droidvm/lib/store/vm/VMCreationDefaultsTest.java index 8b7200fc7..fc8ef635a 100644 --- a/app/src/test/java/cn/classfun/droidvm/lib/store/vm/VMCreationDefaultsTest.java +++ b/app/src/test/java/cn/classfun/droidvm/lib/store/vm/VMCreationDefaultsTest.java @@ -4,15 +4,23 @@ package cn.classfun.droidvm.lib.store.vm; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; import org.junit.Test; +import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; + import cn.classfun.droidvm.lib.data.HostAudioDevices; public final class VMCreationDefaultsTest { @Test public void requestedCreateVmDefaultsAreStable() { assertEquals(60, VMScreenConfig.NEW_VM_DEFAULT_POLL_HZ); + assertEquals("127.0.0.1", VMScreenConfig.NEW_VM_DEFAULT_VNC_HOST); + assertEquals(5900, VMScreenConfig.newVmDefaultVncPort(VMScreenConfig.ID_GPU0)); + assertEquals(5909, VMScreenConfig.newVmDefaultVncPort(VMScreenConfig.ID_SIMPLEFB)); var sound = VMPeripheralConfig.createDefaultVirtioSound(); assertEquals(PeripheralType.VIRTIO_SOUND, sound.getType()); @@ -25,4 +33,67 @@ public void requestedCreateVmDefaultsAreStable() { assertEquals(HostAudioDevices.SYSTEM_DEFAULT_KEY, endpoints.get(1).getHostDevice()); } + /** + * What Customize opens with, against what the editor would fall back to on its own. + * + *

Every field the editor shows has up to three defaults behind it: the value this factory + * writes into the config, the fallback the editor passes to {@code optLong} when the config + * does not mention the key, and the {@code android:text} in the layout. They are read in that + * order, so the first one present wins and the ones below it are never seen. That is fine + * while they agree and invisible when they do not -- the config said the virtio-gpu screen was + * bound to nothing while the editor's own row said NATIVE, and because the config is loaded + * over the row, turning the device on showed "sink" with no way to tell which layer said so. + * + *

So: the screens' exporter comes from one shared constant, and the graphics pools are the + * editor's to default. A pool key appearing here means this test has to say what the editor + * should show for it -- which is the point, because that is the moment the two can disagree. + */ + @Test + public void customizeDefaultsAgreeWithTheEditorsOwn() { + // Only LendMthpMode.defaultForDevice reads it, inside its own try/catch. + var item = VMConfig.createWithCustomizeDefaults(null).item; + + var gpu = VMScreenConfig.find(item, VMScreenConfig.ID_GPU0); + assertNotNull(gpu); + // Off: a new VM has no virtio-gpu device until asked. Bound anyway, because this is what + // the editor shows the moment it is asked, and save() writes NONE while it stays off. + assertFalse(gpu.isEnabled()); + assertEquals(VMScreenConfig.NEW_VM_DEFAULT_EXPORTER, gpu.getExporter()); + // The ladder belongs to the edge, so the stored ceiling has to be the one computed for the + // exporter stored beside it. These two came apart once -- the cap was worked out for + // NATIVE while the exporter said NONE -- and nothing downstream could notice, because each + // is separately valid. Which rung that is per exporter is DisplayTransportCapTest's + // subject, including that switching to VNC moves it to the hardware encoder. + assertEquals(DisplayTransportCap.defaultFor( + VMScreenConfig.ID_GPU0, VMScreenConfig.NEW_VM_DEFAULT_EXPORTER), + gpu.getTransportCap()); + // Written even while this screen is off and bound to something that is not VNC, for the + // same reason the exporter above it is: the editor loads this config over its rows, so + // this is what it shows the moment the user switches the screen to VNC. The two screens' + // ports differ because both can be exported at once and two servers may not share one. + assertEquals(VMScreenConfig.NEW_VM_DEFAULT_VNC_HOST, gpu.getVncHost()); + assertEquals(VMScreenConfig.NEW_VM_DEFAULT_VNC_PORT_GPU0, gpu.getVncPort()); + + var fb = VMScreenConfig.find(item, VMScreenConfig.ID_SIMPLEFB); + assertNotNull(fb); + assertTrue(fb.isEnabled()); + assertEquals(VMScreenConfig.NEW_VM_DEFAULT_EXPORTER, fb.getExporter()); + assertEquals(DisplayTransportCap.defaultFor( + VMScreenConfig.ID_SIMPLEFB, VMScreenConfig.NEW_VM_DEFAULT_EXPORTER), + fb.getTransportCap()); + assertEquals(VMScreenConfig.NEW_VM_DEFAULT_VNC_HOST, fb.getVncHost()); + assertEquals(VMScreenConfig.NEW_VM_DEFAULT_VNC_PORT_SIMPLEFB, fb.getVncPort()); + + // The graphics pools: the editor owns these defaults (VMEditGraphicsTab.loadConfigLocked + // passes each one to optLong), and the layout repeats them. Writing one here would take + // that decision away silently, so if this list has to change, change the editor with it. + for (var key : new String[] { + "gpu_drm2kgsl_pool_mb", "gpu_host_pool_mb", "gpu_venus_pool_mb", + "gpu_guest_pool_mb", "gpu_guest_prealloc_mb", "gpu_guest_step_mb", + "gpu_guest_max_grants", "gpu_pool_blob_max_kb", "gpu_vram_quota_mb", + }) { + assertEquals(fmt("%s is the editor's default to make, not this factory's", key), + -1L, item.optLong(key, -1L)); + } + } } diff --git a/app/src/test/java/cn/classfun/droidvm/lib/store/vm/VMScreenMigrationTest.java b/app/src/test/java/cn/classfun/droidvm/lib/store/vm/VMScreenMigrationTest.java new file mode 100644 index 000000000..c07e3237b --- /dev/null +++ b/app/src/test/java/cn/classfun/droidvm/lib/store/vm/VMScreenMigrationTest.java @@ -0,0 +1,507 @@ +package cn.classfun.droidvm.lib.store.vm; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; + +import org.junit.Test; + +import java.nio.charset.StandardCharsets; +import java.util.HashSet; +import java.util.UUID; + +import cn.classfun.droidvm.lib.store.base.DataItem; +import cn.classfun.droidvm.lib.store.enums.Enums; + +/** + * The one-way fold of the legacy display keys into per-screen bindings. Every case here is a + * config shape the editor could actually write, including the two the old keys allowed by + * accident (display off with VNC on; a backend the GPU switch does not back). + */ +public class VMScreenMigrationTest { + private static DataItem legacy(boolean displayEnabled, DisplayBackend backend, + boolean nativeDisplay, boolean vnc) { + var item = DataItem.newObject(); + item.set("display_enabled", displayEnabled); + item.set("display_backend", backend); + item.set("native_display_enabled", nativeDisplay); + item.set("vnc_enabled", vnc); + return item; + } + + private static VMScreenConfig gpu0(DataItem item) { + return VMScreenConfig.find(item, VMScreenConfig.ID_GPU0); + } + + private static VMScreenConfig fb(DataItem item) { + return VMScreenConfig.find(item, VMScreenConfig.ID_SIMPLEFB); + } + + @Test + public void windowsStyleSimplefbNative() { + var item = legacy(true, DisplayBackend.SIMPLEFB, true, false); + VMScreenConfig.migrate(item); + assertFalse(gpu0(item).isEnabled()); + assertEquals(DisplayExporter.NONE, gpu0(item).getExporter()); + assertTrue(fb(item).isEnabled()); + assertEquals(DisplayExporter.NATIVE, fb(item).getExporter()); + } + + @Test + public void linuxStyleVirtioGpuVnc() { + var item = legacy(true, DisplayBackend.VIRTIO_GPU, false, true); + item.set("gpu_enabled", true); + item.set("vnc_host", "0.0.0.0"); + item.set("vnc_port", 5901); + item.set("vnc_password_auth", true); + item.set("vnc_password", "hunter2!"); + VMScreenConfig.migrate(item); + assertTrue(gpu0(item).isEnabled()); + assertEquals(DisplayExporter.VNC, gpu0(item).getExporter()); + assertEquals("0.0.0.0", gpu0(item).getVncHost()); + assertEquals(5901, gpu0(item).getVncPort()); + assertTrue(gpu0(item).isVncPasswordAuth()); + assertEquals("hunter2!", gpu0(item).getVncPassword()); + assertFalse(fb(item).isEnabled()); + } + + @Test + public void nativeWinsOverVncOnTheSameScreen() { + // The old pair could say both; crosvm silently kept VNC and the app's Surface never got a + // binder. One exporter per screen means the fold has to choose, and it chooses what the + // editor's own read-back did. + var item = legacy(true, DisplayBackend.VIRTIO_GPU, true, true); + item.set("gpu_enabled", true); + VMScreenConfig.migrate(item); + assertEquals(DisplayExporter.NATIVE, gpu0(item).getExporter()); + assertEquals(DisplayExporter.NONE, fb(item).getExporter()); + } + + @Test + public void backendNoneBindsNothing() { + var item = legacy(true, DisplayBackend.NONE, true, false); + VMScreenConfig.migrate(item); + assertFalse(gpu0(item).isEnabled()); + assertFalse(fb(item).isEnabled()); + assertEquals(DisplayExporter.NONE, gpu0(item).getExporter()); + assertEquals(DisplayExporter.NONE, fb(item).getExporter()); + } + + @Test + public void displayOffWithVncOnKeepsWorking() { + // buildVncCommand never looked at display_enabled, so this VM had a working VNC server on + // the GPU device's default display. It keeps one, bound where crosvm's own compat rule + // for an unscreened exporter would have put it. + var item = legacy(false, DisplayBackend.NONE, false, true); + item.set("gpu_enabled", true); + item.set("vnc_port", 5902); + VMScreenConfig.migrate(item); + assertTrue(gpu0(item).isEnabled()); + assertEquals(DisplayExporter.VNC, gpu0(item).getExporter()); + assertEquals(5902, gpu0(item).getVncPort()); + } + + @Test + public void displayOffWithVncOnAndNoGpuBindsNothingButKeepsSettings() { + var item = legacy(false, DisplayBackend.NONE, false, true); + item.set("vnc_port", 5903); + item.set("vnc_password", "keepme12"); + VMScreenConfig.migrate(item); + assertEquals(DisplayExporter.NONE, gpu0(item).getExporter()); + assertEquals(DisplayExporter.NONE, fb(item).getExporter()); + assertEquals(5903, fb(item).getVncPort()); + assertEquals("keepme12", fb(item).getVncPassword()); + } + + @Test + public void legacyKeysAreGoneAfterTheFold() { + var item = legacy(true, DisplayBackend.SIMPLEFB, true, false); + item.set("vnc_port", 5900); + VMScreenConfig.migrate(item); + for (var key : new String[]{"display_enabled", "display_backend", "native_display_enabled", + "vnc_enabled", "vnc_host", "vnc_port", "vnc_password_auth", "vnc_password"}) + assertNull(item.opt(key, (DataItem) null)); + } + + @Test + public void aConfigAlreadyInTheNewShapeIsLeftAlone() { + var item = DataItem.newObject(); + var fb = VMScreenConfig.of(item, VMScreenConfig.ID_SIMPLEFB); + fb.setEnabled(true); + fb.setExporter(DisplayExporter.NATIVE); + // A stray legacy key next to it must not re-run the fold and overwrite the binding. + item.set("display_backend", DisplayBackend.VIRTIO_GPU); + VMScreenConfig.migrate(item); + assertTrue(fb(item).isEnabled()); + assertEquals(DisplayExporter.NATIVE, fb(item).getExporter()); + } + + @Test + public void theGpuScreenSwitchIsTheDeviceAndNothingElseIsAsked() { + // There used to be a second answer to "does this VM have a GPU" sitting next to this one, + // and the pair could disagree. One switch means the question has one place to be asked. + var item = legacy(true, DisplayBackend.VIRTIO_GPU, true, false); + item.set("gpu_enabled", true); + VMScreenConfig.migrate(item); + assertTrue(gpu0(item).isEnabled()); + assertTrue(VMScreenConfig.hasGpuDevice(item)); + assertEquals(1, VMScreenConfig.boundOf(item).size()); + assertNull(item.opt("gpu_enabled", (DataItem) null)); + gpu0(item).setEnabled(false); + assertFalse(VMScreenConfig.hasGpuDevice(item)); + assertTrue(VMScreenConfig.boundOf(item).isEmpty()); + } + + @Test + public void acceleratorOnWithTheScreenOffLeavesNoDevice() { + // The acceleration-test shape: a GPU meant to render while scanning out nothing. No guest + // desktop ever came up on one, so the fold does not carry it forward -- and a VM that was + // not doing anything is better described as off than preserved because it was written down. + var item = legacy(true, DisplayBackend.SIMPLEFB, true, false); + item.set("gpu_enabled", true); + item.set("gpu_backend", GpuBackend.GPU_GFXSTREAM); + VMScreenConfig.migrate(item); + assertFalse(gpu0(item).isEnabled()); + assertFalse(VMScreenConfig.hasGpuDevice(item)); + // The simplefb half of that VM is untouched: it is the half that was displaying anything. + assertTrue(fb(item).isEnabled()); + assertEquals(DisplayExporter.NATIVE, fb(item).getExporter()); + // The renderer choice stays on disk, unread, so turning the device back on in the editor + // finds it rather than a default. + assertEquals(GpuBackend.GPU_GFXSTREAM, Enums.optEnum(item, "gpu_backend", GpuBackend.NONE)); + } + + @Test + public void acceleratorOffWithTheScreenOnBecomesA2dDevice() { + // The reverse disagreement was an ordinary VM in intent -- a display, no 3D -- but the old + // builder emitted no --gpu at all for it, so it had no display device either. The new + // model can say what was meant, and saying it is also the repair. + var item = legacy(true, DisplayBackend.VIRTIO_GPU, false, true); + item.set("gpu_enabled", false); + VMScreenConfig.migrate(item); + assertTrue(gpu0(item).isEnabled()); + assertEquals(GpuBackend.GPU_2D, Enums.optEnum(item, "gpu_backend", GpuBackend.NONE)); + assertNull(item.opt("gpu_enabled", (DataItem) null)); + } + + @Test + public void aConfigThatNeverNamedAnAcceleratorAtAllStillGetsARenderer() { + // gpu_enabled absent reads as false, which is the same VM as the case above; gating the + // fold on the key alone would leave this one with a virtio-gpu screen and no renderer. + var item = legacy(true, DisplayBackend.VIRTIO_GPU, true, false); + assertNull(item.opt("gpu_enabled", (DataItem) null)); + VMScreenConfig.migrate(item); + assertTrue(gpu0(item).isEnabled()); + assertEquals(GpuBackend.GPU_2D, Enums.optEnum(item, "gpu_backend", GpuBackend.NONE)); + } + + @Test + public void theFlatGeometryLandsOnBothScreensAndTheOldKeysGo() { + // One width could not be two, so both screens inherit the single answer the old schema + // could give and are free to disagree from the next edit onwards. + var item = legacy(true, DisplayBackend.VIRTIO_GPU, true, false); + item.set("gpu_enabled", true); + item.set("display_width", 1400); + item.set("display_height", 1050); + item.set("display_refresh_rate", 60); + item.set("display_dpi_h", 220); + item.set("display_dpi_v", 210); + VMScreenConfig.migrate(item); + + assertEquals(1400, gpu0(item).getWidth()); + assertEquals(1050, gpu0(item).getHeight()); + assertEquals(60, gpu0(item).getRefreshRate()); + assertEquals(220, gpu0(item).getDpiH()); + assertEquals(210, gpu0(item).getDpiV()); + + assertEquals(1400, fb(item).getWidth()); + assertEquals(1050, fb(item).getHeight()); + // simplefb gets what it can hold and no more: the device tree names no rate and no DPI, + // so the mode fields would be numbers nothing reads. What it gains instead is a poll rate. + assertEquals(VMScreenConfig.DEFAULT_POLL_HZ, fb(item).getPollHz()); + assertNull(fb(item).item.opt("refresh_rate", (DataItem) null)); + assertNull(fb(item).item.opt("dpi_h", (DataItem) null)); + + for (var key : new String[]{"display_width", "display_height", "display_refresh_rate", + "display_dpi_h", "display_dpi_v"}) + assertNull(item.opt(key, (DataItem) null)); + } + + @Test + public void aPartialFlatGeometryFillsTheRestFromTheOldDefaults() { + // The old reads all carried defaults, so a config naming only some of the five was not a + // config with holes in it -- it was one that agreed with the defaults for the rest. + var item = legacy(true, DisplayBackend.SIMPLEFB, false, true); + item.set("display_width", 800); + VMScreenConfig.migrate(item); + assertEquals(800, fb(item).getWidth()); + assertEquals(VMScreenConfig.DEFAULT_HEIGHT, fb(item).getHeight()); + assertEquals(800, gpu0(item).getWidth()); + assertEquals(VMScreenConfig.DEFAULT_REFRESH_RATE, gpu0(item).getRefreshRate()); + assertEquals(VMScreenConfig.DEFAULT_DPI, gpu0(item).getDpiH()); + } + + @Test + public void theGeometryFoldDoesNotRunTwiceOverAnEditedScreen() { + // Gated on the legacy keys still being there, not on the new ones being absent: the new + // defaults equal the old values, so "absent" cannot be told from "folded and left alone", + // and gating on that would walk back over a size the user has since changed. + var item = legacy(true, DisplayBackend.VIRTIO_GPU, true, false); + item.set("gpu_enabled", true); + item.set("display_width", 1400); + VMScreenConfig.migrate(item); + gpu0(item).setWidth(2560); + VMScreenConfig.migrate(item); + assertEquals(2560, gpu0(item).getWidth()); + } + + @Test + public void aScreenWithNoPollRateStillAnswersWithTheRateTheBridgeUsed() { + var item = DataItem.newObject(); + var fb = VMScreenConfig.of(item, VMScreenConfig.ID_SIMPLEFB); + assertEquals(30, VMScreenConfig.DEFAULT_POLL_HZ); + assertEquals(VMScreenConfig.DEFAULT_POLL_HZ, fb.getPollHz()); + // The bounds are crosvm's: 1 is "look once a second at worst", and above 240 the watcher + // is asking for more work than anything downstream can use. + assertEquals(1, VMScreenConfig.MIN_POLL_HZ); + assertEquals(240, VMScreenConfig.MAX_POLL_HZ); + assertTrue(VMScreenConfig.DEFAULT_POLL_HZ >= VMScreenConfig.MIN_POLL_HZ); + assertTrue(VMScreenConfig.DEFAULT_POLL_HZ <= VMScreenConfig.MAX_POLL_HZ); + fb.setPollHz(10); + assertEquals(10, fb.getPollHz()); + } + + @Test + public void serviceNamesAreOnePerScreenAndReverseToTheVm() { + var vmId = "7f3c1c22-0a11-4d55-9f0e-2b0d5a6e1234"; + var gpuName = NativeDisplay.serviceNameFromId(vmId, VMScreenConfig.ID_GPU0); + var fbName = NativeDisplay.serviceNameFromId(vmId, VMScreenConfig.ID_SIMPLEFB); + assertEquals("droidvm_disp_7f3c1c22-0a11-4d55-9f0e-2b0d5a6e1234_gpu-0", gpuName); + assertEquals("droidvm_disp_7f3c1c22-0a11-4d55-9f0e-2b0d5a6e1234_simplefb", fbName); + assertEquals(vmId, NativeDisplay.vmIdFromServiceName(gpuName)); + assertEquals(vmId, NativeDisplay.vmIdFromServiceName(fbName)); + // The pre-screens name still resolves, so a console left over from an older build does + // not make the daemon refuse to wait. + assertEquals(vmId, NativeDisplay.vmIdFromServiceName(NativeDisplay.channelKeyFromId(vmId))); + assertEquals("", NativeDisplay.vmIdFromServiceName("android.hardware.something")); + } + + @Test + public void inputIsOnForAConfigThatNeverHeardOfTheKey() { + // The whole migration of this attribute is the default: a config written before the key + // existed had the devices, and reading it must not take them away. Nothing rewrites the + // file to say so, so the absent key has to answer for itself. + var item = legacy(true, DisplayBackend.SIMPLEFB, true, false); + VMScreenConfig.migrate(item); + assertTrue(fb(item).isInputEnabled()); + assertNull(fb(item).item.opt("input_enabled", (DataItem) null)); + assertTrue(fb(item).hasAbsoluteInput()); + assertEquals(1, VMScreenConfig.absoluteInputOf(item).size()); + assertEquals(VMScreenConfig.ID_SIMPLEFB, VMScreenConfig.absoluteInputOf(item).get(0).id); + } + + @Test + public void switchingInputOffDropsOnlyThatScreensAbsoluteDevices() { + var item = DataItem.newObject(); + for (var id : VMScreenConfig.IDS) { + var screen = VMScreenConfig.of(item, id); + screen.setEnabled(true); + screen.setExporter(DisplayExporter.NATIVE); + } + assertEquals(2, VMScreenConfig.absoluteInputOf(item).size()); + VMScreenConfig.of(item, VMScreenConfig.ID_GPU0).setInputEnabled(false); + var left = VMScreenConfig.absoluteInputOf(item); + assertEquals(1, left.size()); + assertEquals(VMScreenConfig.ID_SIMPLEFB, left.get(0).id); + // The screen is still a screen with an exporter -- only its two absolute devices went. + // The keyboard and the relative pointer are the VM's, so nothing here can speak for them. + assertTrue(VMScreenConfig.find(item, VMScreenConfig.ID_GPU0).isEnabled()); + assertEquals(2, VMScreenConfig.boundOf(item).size()); + } + + @Test + public void aScreenNobodyWatchesGetsNoAbsoluteDevices() { + // Input on, device present, and still nothing to emit: with no exporter there is no + // console to send absolute events from, so the devices would be unreachable. + var item = DataItem.newObject(); + var fb = VMScreenConfig.of(item, VMScreenConfig.ID_SIMPLEFB); + fb.setEnabled(true); + fb.setInputEnabled(true); + fb.setExporter(DisplayExporter.NONE); + assertFalse(fb.hasAbsoluteInput()); + assertTrue(VMScreenConfig.absoluteInputOf(item).isEmpty()); + } + + @Test + public void scanoutSocketsAreOnePerScreenAndOnlyTheRelativePointerIgnoresTheScreen() { + var vmId = "7f3c1c22-0a11-4d55-9f0e-2b0d5a6e1234"; + var run = fmt("/data/data/cn.classfun.droidvm/run/dvmin_%s", vmId); + // Exact names, because these are a contract between two processes: the daemon binds the + // inode and crosvm is handed the path on its command line. A rename that only one side + // learns about is a VM that does not start, so it should fail here first. + assertEquals(fmt("%s_sfb_mt.sock", run), NativeDisplay.inputSocketPath( + vmId, VMScreenConfig.ID_SIMPLEFB, NativeDisplay.MULTITOUCH)); + assertEquals(fmt("%s_g0_tab.sock", run), NativeDisplay.inputSocketPath( + vmId, VMScreenConfig.ID_GPU0, NativeDisplay.TABLET)); + assertEquals(fmt("%s_g0_kbd.sock", run), NativeDisplay.inputSocketPath( + vmId, VMScreenConfig.ID_GPU0, NativeDisplay.KEYBOARD)); + assertEquals(fmt("%s_ms.sock", run), + NativeDisplay.inputSocketPath(vmId, "", NativeDisplay.MOUSE)); + // Each screen's devices are its own inodes. For the two absolute ones that is because a + // coordinate only means anything under one output; for the keyboard it is because input + // belongs to the scanout, so the screen's switch has to be able to silence typing on that + // screen alone -- a shared keyboard could not be switched off by any one screen. + for (var ch : new int[]{ + NativeDisplay.MULTITOUCH, NativeDisplay.TABLET, NativeDisplay.KEYBOARD}) { + assertTrue(NativeDisplay.isPerScreen(ch)); + assertNotEquals(NativeDisplay.inputSocketPath(vmId, VMScreenConfig.ID_GPU0, ch), + NativeDisplay.inputSocketPath(vmId, VMScreenConfig.ID_SIMPLEFB, ch)); + } + // The relative pointer is the one device with no output binding -- the guest routes it by + // focus and it walks between outputs -- so which screen the console is showing must not + // reach its socket name at all. + assertFalse(NativeDisplay.isPerScreen(NativeDisplay.MOUSE)); + assertEquals(NativeDisplay.inputSocketPath(vmId, "", NativeDisplay.MOUSE), + NativeDisplay.inputSocketPath(vmId, VMScreenConfig.ID_GPU0, NativeDisplay.MOUSE)); + assertEquals( + NativeDisplay.inputSocketPath(vmId, VMScreenConfig.ID_SIMPLEFB, NativeDisplay.MOUSE), + NativeDisplay.inputSocketPath(vmId, VMScreenConfig.ID_GPU0, NativeDisplay.MOUSE)); + // Seven sockets for a two-screen VM -- three devices per screen plus the VM's pointer -- + // and no two of them the same file. + var seen = new HashSet(); + for (int ch = 0; ch < NativeDisplay.CHANNEL_COUNT; ch++) + for (var id : VMScreenConfig.IDS) + seen.add(NativeDisplay.inputSocketPath(vmId, id, ch)); + assertEquals(7, seen.size()); + } + + @Test + public void socketFilenamesAreCompactEnoughToBindWithMarginLeft() { + // The names this asserts on are short because the long ones did not work. sun_path holds + // 107 bytes plus a NUL; /data/data/cn.classfun.droidvm/run/ spends 35 of them before a + // name starts; the pre-screens droidvm_disp__input_multitouch.sock came to 106, one + // byte of headroom, so inserting the screen id made it 115 (111 for the tablet) and crosvm + // refused the command line with "path must be shorter than SUN_LEN". + // + // So measure against the real run directory rather than a stub -- half the budget is the + // base dir, and a test that supplies its own would not have caught this -- and hold the + // result to 100, well under the limit, so the next name that grows has somewhere to grow. + var vmId = UUID.randomUUID().toString(); + assertEquals(36, vmId.length()); + var sample = NativeDisplay.inputSocketPath(vmId, VMScreenConfig.ID_GPU0, + NativeDisplay.MULTITOUCH); + assertEquals("/data/data/cn.classfun.droidvm/run/", + sample.substring(0, sample.lastIndexOf('/') + 1)); + var worst = ""; + for (int ch = 0; ch < NativeDisplay.CHANNEL_COUNT; ch++) { + for (var id : VMScreenConfig.IDS) { + var path = NativeDisplay.inputSocketPath(vmId, id, ch); + if (bytes(path) > bytes(worst)) worst = path; + } + } + assertEquals(90, bytes(worst)); + assertTrue(worst, bytes(worst) <= 100); + assertTrue(worst, bytes(worst) <= NativeDisplay.MAX_UNIX_PATH); + // And the check the daemon runs before binding agrees with all of them. + for (int ch = 0; ch < NativeDisplay.CHANNEL_COUNT; ch++) + for (var id : VMScreenConfig.IDS) + NativeDisplay.requireBindablePath(NativeDisplay.inputSocketPath(vmId, id, ch)); + } + + @Test + public void anOverlongSocketPathIsRefusedInsteadOfBeingTruncated() { + // bind(2) copies the path into a 108-byte sun_path and truncates without a word, so the + // daemon would listen on one inode while crosvm connects to the name it was given -- the + // two stubs the failing build left in run/, "..._simplefb_input_multito" and + // "..._simplefb_input_tablet.", were the only evidence it had happened. The check has to + // fire before the syscall, and it has to say which path and how long. + var longest = fmt("%sy", "/x".repeat(53)); + assertEquals(NativeDisplay.MAX_UNIX_PATH, longest.length()); + assertEquals(longest, NativeDisplay.requireBindablePath(longest)); + var overlong = fmt("%sz", longest); + var e = assertThrows(IllegalArgumentException.class, + () -> NativeDisplay.requireBindablePath(overlong)); + assertTrue(e.getMessage(), e.getMessage().contains(overlong)); + assertTrue(e.getMessage(), e.getMessage().contains("108")); + // Bytes, not chars: sun_path is a byte array, and a name that is short in code points can + // still be too long for it. + var wide = "/" + "\u00e9".repeat(NativeDisplay.MAX_UNIX_PATH - 1); + assertEquals(NativeDisplay.MAX_UNIX_PATH, wide.length()); + assertThrows(IllegalArgumentException.class, + () -> NativeDisplay.requireBindablePath(wide)); + } + + private static int bytes(String s) { + return s.getBytes(StandardCharsets.UTF_8).length; + } + + @Test + public void touchDeviceNamesAreDerivedFromTheScreenAndNothingElse() { + // The guest maps a touchscreen to an output by this string and stores it by this string, + // so it must be a pure function of the screen id -- no VM, no index, no ordering. + assertEquals("DroidVM Touch (gpu-0)", + NativeDisplay.touchDeviceName(VMScreenConfig.ID_GPU0)); + assertEquals("DroidVM Touch (simplefb)", + NativeDisplay.touchDeviceName(VMScreenConfig.ID_SIMPLEFB)); + assertNotEquals(NativeDisplay.touchDeviceName(VMScreenConfig.ID_GPU0), + NativeDisplay.touchDeviceName(VMScreenConfig.ID_SIMPLEFB)); + } + + @Test + public void tabletDeviceNamesAreDerivedTooAndNeverCollideWithTheTouchscreen() { + // The tablet is as much a per-output device as the touchscreen, so it is derived the same + // way. It also has to be distinguishable from its own screen's touchscreen: the two sit + // side by side in the guest's device list and the user picks between them there. + assertEquals("DroidVM Tablet (gpu-0)", + NativeDisplay.tabletDeviceName(VMScreenConfig.ID_GPU0)); + assertEquals("DroidVM Tablet (simplefb)", + NativeDisplay.tabletDeviceName(VMScreenConfig.ID_SIMPLEFB)); + assertNotEquals(NativeDisplay.tabletDeviceName(VMScreenConfig.ID_GPU0), + NativeDisplay.tabletDeviceName(VMScreenConfig.ID_SIMPLEFB)); + for (var id : new String[] {VMScreenConfig.ID_GPU0, VMScreenConfig.ID_SIMPLEFB}) { + assertNotEquals(NativeDisplay.touchDeviceName(id), NativeDisplay.tabletDeviceName(id)); + } + } + + @Test + public void keyboardDeviceNamesAreDerivedPerScreenAndMatchTheCrosvmSeamFormat() { + // Exact strings: crosvm builds a VNC-exported screen's keyboard itself and names it by + // reproducing this format, with no command-line key carrying it between the two repos. So + // the format IS the contract, and a change here that crosvm does not learn about leaves + // the guest with two differently-named keyboards for what the user thinks is one setup. + assertEquals("DroidVM Keyboard (gpu-0)", + NativeDisplay.keyboardDeviceName(VMScreenConfig.ID_GPU0)); + assertEquals("DroidVM Keyboard (simplefb)", + NativeDisplay.keyboardDeviceName(VMScreenConfig.ID_SIMPLEFB)); + assertNotEquals(NativeDisplay.keyboardDeviceName(VMScreenConfig.ID_GPU0), + NativeDisplay.keyboardDeviceName(VMScreenConfig.ID_SIMPLEFB)); + // And it is distinguishable from the screen's other two devices, which sit beside it in + // the guest's device list. + for (var id : new String[] {VMScreenConfig.ID_GPU0, VMScreenConfig.ID_SIMPLEFB}) { + assertNotEquals(NativeDisplay.keyboardDeviceName(id), NativeDisplay.touchDeviceName(id)); + assertNotEquals(NativeDisplay.keyboardDeviceName(id), + NativeDisplay.tabletDeviceName(id)); + } + } + + @Test + public void perScreenNamesCarryNoCommaThatWouldSplitAnInputOption() { + // Both names are interpolated into a crosvm `--input kind[path=...,name=...]` option, whose + // key-value parser runs an unquoted value to the next ',' or ']'. Spaces and parentheses + // survive that; a ',' or a bracket in a screen id would silently truncate the name into a + // different device, so assert the characters that would do it never appear. + for (var id : new String[] {VMScreenConfig.ID_GPU0, VMScreenConfig.ID_SIMPLEFB}) { + for (var name : new String[] { + NativeDisplay.touchDeviceName(id), NativeDisplay.tabletDeviceName(id), + NativeDisplay.keyboardDeviceName(id)}) { + assertEquals(-1, name.indexOf(',')); + assertEquals(-1, name.indexOf('[')); + assertEquals(-1, name.indexOf(']')); + } + } + } +} diff --git a/app/src/test/java/cn/classfun/droidvm/ui/vm/edit/graphics/ScreenResolutionOptionsTest.java b/app/src/test/java/cn/classfun/droidvm/ui/vm/edit/graphics/ScreenResolutionOptionsTest.java new file mode 100644 index 000000000..4f8709c8f --- /dev/null +++ b/app/src/test/java/cn/classfun/droidvm/ui/vm/edit/graphics/ScreenResolutionOptionsTest.java @@ -0,0 +1,83 @@ +package cn.classfun.droidvm.ui.vm.edit.graphics; + +import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import androidx.annotation.NonNull; + +import org.junit.Test; + +import java.util.List; + +import cn.classfun.droidvm.ui.vm.edit.graphics.ScreenResolutionOptions.Option; + +/** + * The resolution dropdown's list: what it offers on a given panel, and in what order. + * + *

The rules are all here rather than in the row because they are the half of that dropdown that + * does not need a device -- and because the two that depend on the panel land in a different place + * on every device, which is the sort of thing a test says once and a screenshot never settles.

+ */ +public class ScreenResolutionOptionsTest { + @Test + public void ordersEverythingByArea() { + // 1080x2400 portrait panel: half of it (1200x540) is smaller than either fixed size, and + // the panel itself is larger than both -- so the two device sizes bracket the fixed ones. + assertSizes(ScreenResolutionOptions.build(1080, 2400), + new int[][]{{1200, 540}, {1280, 720}, {1920, 1080}, {2400, 1080}}); + } + + @Test + public void everySizeIsLandscape() { + for (var panel : new int[][]{{1080, 2400}, {2400, 1080}, {1440, 3200}, {800, 1280}}) + for (var opt : ScreenResolutionOptions.build(panel[0], panel[1])) + assertTrue(fmt("%dx%d", opt.width, opt.height), opt.width >= opt.height); + } + + @Test + public void readsThePanelTheSameInEitherRotation() { + assertSizes(ScreenResolutionOptions.build(2400, 1080), + new int[][]{{1200, 540}, {1280, 720}, {1920, 1080}, {2400, 1080}}); + } + + @Test + public void withoutAPanelOnlyTheFixedSizesAreOffered() { + assertSizes(ScreenResolutionOptions.build(0, 0), new int[][]{{1280, 720}, {1920, 1080}}); + assertSizes(ScreenResolutionOptions.build(-1, 2400), new int[][]{{1280, 720}, {1920, 1080}}); + } + + @Test + public void aSizeTwoRulesProduceIsListedOnce() { + // A 1080p panel: "1920x1080" and "the phone's own" are the same size. + assertSizes(ScreenResolutionOptions.build(1920, 1080), + new int[][]{{960, 540}, {1280, 720}, {1920, 1080}}); + + // A 1440p panel halves to exactly 1280x720, which the fixed list already had. + assertSizes(ScreenResolutionOptions.build(1440, 2560), + new int[][]{{1280, 720}, {1920, 1080}, {2560, 1440}}); + } + + @Test + public void halvingAnOddPanelRoundsDownToEven() { + var opts = ScreenResolutionOptions.build(1179, 2556); + assertEquals(1278, opts.get(0).width); + assertEquals(588, opts.get(0).height); + } + + @Test + public void aHalfBelowTheValidatorsFloorIsNotOffered() { + // 854x480 halves to 427x240, which the editor would refuse to save. The panel itself still + // stands: it is over the floor. + assertSizes(ScreenResolutionOptions.build(480, 854), + new int[][]{{854, 480}, {1280, 720}, {1920, 1080}}); + } + + private static void assertSizes(@NonNull List