From 3d010e6fe0c6a0c3be8b2bef5c588f91245e9898 Mon Sep 17 00:00:00 2001 From: HuJK Date: Sun, 30 Aug 2026 13:37:07 +0800 Subject: [PATCH 01/40] 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 00000000..987ba260 --- /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 00000000..903a743a --- /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 00000000..bedb1659 --- /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 4d6a266e..eaa08842 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 6db9c122..8389dfb5 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 023fbe25..ca69dbb7 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 3525a905..d1ed9831 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 7e4fe5be..0dd9d22c 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 bb04720e..15d3fd4a 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 5aa42749..dc51f381 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 1f268d98..6b63881c 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 e5e4c1f0..c6bc5667 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 6b866490..65056c6c 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 c918cbf4..400d8bb9 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 989d4377..3e976180 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 33db76b3..20e5a375 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 00733553..b5d64b90 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 2fd9bb10..23161249 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 e2df64f3..640a1097 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 e1ac41fc..924f7731 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 37b3e465..adb641a5 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 5278a111..52e54d06 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 33e19494..2db683ed 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 aeefda1a..262a314d 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 1bda4ec2..a4f11da1 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 98c28625..772d2d25 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 e3569534..8cbf9fa3 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 93bc9245..a13c8f49 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 1a253a94..14f77574 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 e4b9c623..62a4bc21 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 be458a26..c98e0f02 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 bec5959d..e0e920a6 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 712e10ee..ff0cb6cd 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 fe81b800..8fef1942 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 bbd4399e..86dd9777 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 1d09eb7d..07fd26cc 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 e75e2d68..a966ff70 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 2a8460c1..70c8bbb0 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 d3ab3bae..fb571fa6 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 7eceecba..ccbbeb51 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 c3694081..2ce19763 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 d5616a46..82a8d62e 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 6373cd38..3a97efed 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 4ca0bd72..4e30e5c8 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 043f1dd5..e20c94e5 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 2bd6c570..63fdea58 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 dab0c132..a915e0e8 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 44be4feb..f7ae0c49 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 caac7fe4..38829344 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 15f771e4..0bcc6c20 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 83eaf1e4..ef049838 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 8d7b9220..dd8e9ab6 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 fb3af10a..1d131dd7 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 dfd9991d..aca06f6d 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 99922c35..b7bb2ccb 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 650af356..7aaaf686 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 305d8916..174f69c3 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 06af6617..1a072c5b 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 a70a1c98..24d5b3a0 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 30c366e2..21329dd4 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 87715f4f..fccaf77f 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 e69a9975..cecfb4ff 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 40c08c56..cd7e5ad4 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 1786d6c4..82c4c1bc 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 522ec52d..a9bf3996 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 b9d5c41a..f7bf966d 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 c6b8e59b..4f5a8647 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 9d383b58..4f973d9e 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 f33e4e40..c35df558 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 0ae109da..5ace9abd 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 59b7952c..379c8ea9 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 3d08b078..0a0e58ba 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 2d91c8b7..5a2684bd 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 402f8ab4..00744e25 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 e6c8bb6c..95e42cc5 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 23b598f0..379107f3 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 01e16c22..e48a5577 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 053e8187..37db8748 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 f98e19de..0df24ede 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 a8224c47..b8e8424d 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 844e0ba2..3ee600f2 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 c5474f24..bd0a01b8 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 c5286302..273b21da 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 b57ba0dd..4fafb226 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 bb5bbde8..39a68e4f 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 5e0a1b6b..373938c1 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 0aa60ebc..516cc8df 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 061b554e..294084fc 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 30607aab..82c8e9c5 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 39a097b8..2a14a380 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 935fc319..f0ffe455 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 28f4b4ea..79a8d3ca 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 9da40e02..e54d1790 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 b8523620..00f05dd5 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 4d292d0f..8284786f 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 41de6564..85c3b4ba 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 37185f3c..b42f1520 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 af305008..6f62a181 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 af5a3c9b..a6707065 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 6479786d..4bb9fde0 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 c8e33c79..0fef5f5e 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 8a8520c3..7addc1cb 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 83f38224..1611d9cd 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 9f792600..fb8b043f 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 4f47f12a..e175a65e 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 852e863f..6a082d81 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 47fdde35..c6725f77 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 b1f1cabd..753b8c6e 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 cd35cee5..e5f90dc0 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 118fe518..7de20ecd 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 059234a2..fb0c732e 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 451ccfa0..3b3a2648 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 8abc2991..bd6ff1f9 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 6d32422c..4a968d59 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 34f9b55e..71ca3810 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 c47f0120..59ee04be 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 1e81355e..4ffd92ec 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 eba503a7..97b8b078 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 fcd02dfb..d0576735 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 3fb894f4..3588af70 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 3668efab..50a8d69b 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 a210ab14..bc782b65 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 768d3eaf..afcd7ef7 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 5668a2c6..46407334 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 a8f89320..497f8d9f 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 da8923c4..19f32545 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 db4920b3..15913e50 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 29985ee4..d5c84de1 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 1305c2a6..bd62411a 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 51726f2a..2bae82c7 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 851f814e..13549039 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 e1a5fff3..f15f0661 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 bf7577f8..c31177db 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 35c61338..e0bf52f8 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 e551bbcc..96f95ace 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 31f22079..a38248bf 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 cc69844f..7bc35bed 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 5316f484..2dc9b2bd 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 8c55a0d6..72e1147c 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 8a870273..85c64734 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 0d2ff6b2..85b3ccdd 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 6ea6a702..494c8c20 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 9d43a59c..9781a3c8 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 ce361676..787f0720 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 731ab374..894617d4 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 283c03be..090e9a99 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 ad3ac87d..6df9a358 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 90c58aa3..0e05fd4b 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 e293d266..8f81d1ac 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 21b6c52c..3fe34f9e 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 34b0918f..6d154c00 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 70ece34e..8b22d0d1 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 0017d0e6..24c57c5e 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 768b9876..94f0de5b 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 c45a8af7..bda4ca68 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 64956cf5..2e2b69ae 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 397c2b1e..daac35f2 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 eaa90096..b20ece30 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 41646fe3..ce3400d6 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 86c80644..2ede507e 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 4c42bfdf..f95a1a68 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 9fd450b1..d9f15691 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 03849259..e2e350ac 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 72e5ed5c..0325cccf 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 0a8a3e64..103fe830 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 bc3ef75d..8cf230d5 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 4e482d88..50f627ad 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 b5870fb2..19e33428 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 09ea8351..cf6d6395 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 e1de9147..18bda4f2 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 87a1949a..4284dee3 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 89625af0..fff22428 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 95e54110..1b65fd1e 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 f9bd3b99..0491b8c3 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 cc8ac0fd..79b9cdf0 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 1dce22b4..17c1cdf8 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 4832cc1d..28d2abd1 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 11af0711..e8cd851d 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 5918898f..251a0969 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 dea25264..72d41928 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 bd8f10da..1fa5e26b 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 e27728bb..fae94108 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 d44e27af..9ce83272 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 8ba24cfc..7b31d5b4 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 e46628c1..ff565547 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 b2b7f7d8..6e160edb 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 b5a6d013..cd0468e3 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 8bf15f15..ca416143 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 97a0f551..d0f79e19 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 c8d91240..6e1ed58e 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 fb449411..49cc5d62 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 c8628da8..cb5eb7cc 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 d45676c4..2cf7e70d 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 b0445760..4e211b60 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 edca416f..0f39edfd 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 49f2866b..95632dd3 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 03baec59..da921faf 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 9f34edfd..ec6bf289 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 f40c452d..96fd0e77 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 4ef28212..deff965f 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 e592d500..8b91dc74 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 d3be02f9..44cfc51f 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 291ff685..7c127e26 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 06dd953d..dcf25f41 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 e725862e..1e422b98 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 2c7dcb84..ae48272a 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 67c2d5e7..34e46dd3 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 2a62a972..51ecc3a2 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 948b4952..20532c39 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 a78ec638..c20b7b6e 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 2ac024de..c3b32e6f 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 f7abdbf2..f2e6dc21 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 1d7d4878..64ecbb2e 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 314e58b8..e7c73a18 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 2fcaae35..6bb3b4e6 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 2cd6710d..dc404204 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 ff415d81..48ef75cd 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 b7cd64bf..f070de28 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 07e471c3..1c612c02 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 0bcbf66a..9e6d101f 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 d6b64d9e..3ec7c015 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 6ca1f30f..e1604e0a 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 fb1039c6..2c186965 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 571414a4..155a3413 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 6f9075f4..69d017c8 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 ba4d3042..4e48ff6d 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 86d11638..58d9319b 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 f4506352..31359401 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 698e3da1..7e99eec0 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 31b56dfa..c30f1476 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 ee824e39..1f94c1a5 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 649dbc10..3b6608c4 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 3c47dbe7..a77adbb6 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 ed195bbb..c6a03023 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 efaf3736..9300c8ef 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 83f4b35b..8b600e21 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 0b4ca11e..95eb357f 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 61e75783..9a112ac5 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 0e68865a..93194d37 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 5c0683c1..f5475ac3 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 99d71726..d0c37def 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 e64c8100..24406b1c 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 d4a2b9c8..d0f14d0a 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 358f4869..98688864 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 855fb110..ce409a80 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 b372f6bc..fa3a0b82 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 7d1945d7..7a45f26e 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 a6cacee1..b81133c8 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 cbf01d2a..ff56efea 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 c6ab065e..cf1ac9f1 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 c2de9286..7285257c 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 e975d006..5720ddee 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 389b809e..213f354e 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 d4890921..7e4b17f8 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 3df8212d..2b7e27fe 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 19997997..3c65236c 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 f819a584..40e1e49b 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 465057d9..55eba1e6 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 0066b2da..2499caff 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 fc492c61..63e16073 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 39e3dce0..08bf4004 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 45c1f8d6..f78a2c7d 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 8a62760e..0f341cbd 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 ebe80ea9..6e0ea17a 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 6054d46b..28a17a62 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 c5159e51..0f2392c0 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 6edf43ac..7c0e51f9 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 825285c4..87df9380 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 1a56bf0a..ea0d5fa1 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 261e1c59..1ccc5bfc 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 7287fbef..a19f34a9 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 aef1fb61..fc331b32 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 406faec5..e953bce1 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 53f4e980..dccc4d35 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 1e320649..51fe9923 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 e81bcd86..5888c11d 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 94ba80d7..23b1468a 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 3b39c875..7cfbb66c 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 0292a894..5b10d255 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 26144f4f..85cdb263 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 e99d11f9..fe9e9c61 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 d746e1a9..583012f5 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 6f8f0a1d..c5ed5315 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 4aa23645..c4c88250 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 b64ac47e..7b673f05 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 3e6cb949..d76b3c9d 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 15392970..3fd4f534 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 e97e932b..e2355af3 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 3085297f..f1f64f5e 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 f4174e07..a472f668 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 4eaedd55..3bb46698 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 c340258f..4f13bdaf 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 5aeb08da..d02eb48b 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 e70b43af..f2e565cd 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 7632fe3a..48d1ab6a 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 8b35ee3c..4ffd0c27 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 5f7949bb..434a1328 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 617628e2..0ae19e04 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 3ef82c42..c6442c8b 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 0d79233c..dc8f0a98 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 29a66d32..06fcb5b8 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 4e8a4383..8329e223 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 4ba17b64..71d6a505 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 5e640ea7..3c508003 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 3e414990..f6262f68 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 2659a6a2..dd0b4665 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 b419f57b..b470c09b 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 cbd35647..99e90757 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 e2441466..c0f75073 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 226ea9d8..699a52e6 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 23afa2ef..18a5d095 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 2c61ab2b..d9400c56 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 a03eb10f..f1193f7b 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 4e5f20ce..454bc4a1 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 3457dd9d..788bf5b5 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 1a81188b..0a910f4e 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 5003a803..46c07a99 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 907b0df5..24a47af0 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 b3544e10..39b3cc5d 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 e9e0ea39..9a13bfc8 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 018fb1b3..20817500 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 8804b5a8..ab59a574 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 de833127..7728040b 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 cd84b5bb..6d919be5 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 12fcaba1..2dec5a72 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 a8a01463..b23567a8 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 f0cc647e..295d48b2 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 6cd01246..2152723e 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 434f2847..d0f76954 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 5f8233bd..30526ae6 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 79433a4f..48dc5a23 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 d034ee04..d445fd23 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 22921d5e..37c5e845 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 524e5db1..f5e85ea4 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 12f0622f..a6209ade 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 523c758a..f03f9815 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 a78b2d2b..9f3d6bc5 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 ce192c4a..ef001df4 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 644eaf13..22b3a0e2 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 3cbe10f8..f85f30ef 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 d7ef0de4..68abe6f8 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 85a91bc4..e677bf07 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 e5447a5d..4b44ca8e 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 3975fc08..4e490374 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 863a8d86..fefb4c29 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 e177de75..135310e7 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 f76cfa08..1f723117 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 0f2c440d..2bc4c364 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 b9e99b1b..925170a2 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 acb06d0f..c540d5d1 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 a5fba6a9..90b1f82d 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 93a93682..5668040a 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 31ce2dc2..a20aa4cb 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 0d3cbde5..5749d2bf 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 3b8d9e6e..57ee908b 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 c1b775d8..687461dd 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 fcb55ebb..38cbabb9 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 32d0721c..7845e915 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 5c516aa5..d843746b 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 ee99894f..3a717578 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 fa858812..2cdf8871 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 c8271fdc..157c056d 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 b3fcbe2c..e5917b52 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 5cfda47b..ff2c2868 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 c5b82293..0afa0fff 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 74650153..f173cc23 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 11d6522c..2c40e6cc 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 4b89116e..c41205e1 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 9689b550..9db922c3 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 a7b67fab..9d06d3f0 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 fc9a8d29..f3aa62a9 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 733ba390..621c14b8 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 d65d41b4..5df93d36 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 616138e4..13f8c229 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 1a5e4b0c..f3291123 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 8bb604fa..448497ad 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 5cd2dc8d..272f260a 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 03847de1..29349ec4 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 c6d7fc08..d064b330 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 8e56fa27..da7870f8 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 7bcffd49..ca8936e9 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 6547b76b..597abc70 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 1f66ed78..2c62c800 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 efb636443f5401cb284f5b8d08acf1f7d4efc594 Mon Sep 17 00:00:00 2001 From: HuJK Date: Sun, 30 Aug 2026 13:37:07 +0800 Subject: [PATCH 02/40] 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 78e71579..0318d083 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 2625919c..478500f9 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 aac83d6e..8e334528 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 2c31cecd..8631ea61 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 00000000..31e437b5 --- /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 503a8e61..6774fb8e 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 00000000..0141a9bf --- /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 e6354fee..9e95bd61 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 00000000..b0a58bbb --- /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 71810c5e5a54f15af0c03c9832ab4ec6d3b859bc Mon Sep 17 00:00:00 2001 From: HuJK Date: Sun, 30 Aug 2026 13:37:09 +0800 Subject: [PATCH 03/40] 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 65056c6c..25b54b14 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 6f62a181..7312bd6c 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 00000000..52db9bce --- /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 4e211b60..04be7e30 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 c3b32e6f..fc4e6e1b 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 295d48b2..8a742154 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 00000000..2d347c85 --- /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 00000000..46f3395c --- /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 2152723e..18ae8b52 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 00000000..3b7d38eb --- /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 00000000..a25b953d --- /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 00000000..205b5454 --- /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 00000000..ef4adfe2 --- /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 00000000..9f0a0431 --- /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 00000000..e3ae38e0 --- /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 00000000..dd031b6e --- /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 48dc5a23..462883e4 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 00000000..fd34c92c --- /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 00000000..8387d7fc --- /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 00000000..7174ec4a --- /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 37c5e845..7f905be0 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 00000000..4afb7718 --- /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 f5e85ea4..3e068ec1 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 00000000..6a839f1b --- /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 a6209ade..2e9c70b6 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 9f3d6bc5..f43cb89d 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 ef001df4..9b4c8f5d 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 22b3a0e2..f3a9114c 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 f85f30ef..aeed31bf 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 e677bf07..23ca2aee 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 4b44ca8e..5004071d 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 135310e7..ff39f1f1 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 00000000..d73be2a9 --- /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 2bc4c364..f9dbbcf6 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 00000000..e89794c0 --- /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 00000000..e1fec3b7 --- /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 00000000..11ba8015 --- /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 00000000..63ec2dc4 --- /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 00000000..ba38f1bb --- /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 00000000..5648c263 --- /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 00000000..756d47ca --- /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 00000000..b6064b36 --- /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 00000000..b463b6da --- /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 00000000..81990c40 --- /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 00000000..9b685a83 --- /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 00000000..09a8fde6 --- /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 46f49238..413d2426 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 90672de5..38de37dd 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 00000000..60ff859b --- /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 00000000..9b3133b1 --- /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 cb3db115..0aeee3fe 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 @@ + - -