- Faster return to virtual machine.
- Fixed the frozen virtual machine startup dialog when using X11.
- Added audio support for the built-in X11 display.
- Improved notifications.
- Qemu 7.2.22 has been improved and requires manual updating with the Upgrade tool using a command in the Terminal.
This commit is contained in:
An Bui 2026-06-10 15:13:21 +07:00
parent 3b16fd8b7d
commit 09f9654ccd
13 changed files with 239 additions and 98 deletions

View file

@ -32,6 +32,7 @@ body:
options:
- Latest
- Beta
- 4.2.0
- 4.1.0
- 4.0.0
- 3.9.0

View file

@ -127,7 +127,7 @@ QEMU 8.2.0 - 3dfx (only for Vectras VM 2.9.5):
- [See them at the Internet Archive](https://archive.org/details/vectras-vm-x86_64.tar_202603nbab)
QEMU 7.2.22 - 3dfx (for Vectras VM 4.2.2+):
- [For Android ARM (64-bit)](https://archive.org/download/qemu-7-2-22-for-vectras-vm-nbab/base-vectras-vm-arm64-v8a.tar.gz)
- [For Android ARM (64-bit)](https://archive.org/download/qemu-7-2-22-for-vectras-vm-nbab/base-june-2026-vectras-vm-arm64-v8a.tar.gz)
- [See more at the Internet Archive](https://archive.org/details/qemu-7-2-22-for-vectras-vm-nbab)
### 💽 3Dfx Wrappers

View file

@ -12,8 +12,8 @@ android {
applicationId "com.vectras.vm"
minSdk minApi
targetSdk targetApi
versionCode 127
versionName "4.2.3"
versionCode 128
versionName "4.2.4"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true

View file

@ -28,6 +28,8 @@ public class StartVM {
private static DataMainRoms vmConfigs;
public static String env(Activity activity, DataMainRoms vmData) {
if (VMManager.isVMRunning(activity, vmData.vmID)) return "";
vmConfigs = vmData;
if (VMManager.isNeedLoadMigrate() && FileUtils.isFileExists(VmFileManager.getSnapshotSh(Config.vmID))) {

View file

@ -5,6 +5,7 @@ import androidx.annotation.NonNull;
import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;
import com.vectras.vm.R;
import com.vectras.vm.settings.UpdaterActivity;
import com.vectras.vm.utils.NotificationUtils;
import java.util.Map;
@ -22,6 +23,12 @@ public class FCMService
return;
}
Class<?> activityClass = null;
if (data.get("activityClass") != null) {
if (Objects.equals(data.get("activityClass"), "update")) activityClass = UpdaterActivity.class;
}
NotificationUtils.pushNow(this,
1,
NotificationUtils.generalChannelId,
@ -31,7 +38,7 @@ public class FCMService
data.get("image") != null ? data.get("image") : null,
-1,
data.get("url") != null ? data.get("url") : null,
null);
activityClass);
}
@Override

View file

@ -14,7 +14,6 @@ import android.widget.Toast;
import com.vectras.qemu.Config;
import com.vectras.qemu.MainSettingsManager;
import com.vectras.qemu.MainVNCActivity;
import com.vectras.vm.AppConfig;
import com.vectras.vm.MainService;
import com.vectras.vm.R;
import com.vectras.vm.StartVM;
@ -55,6 +54,26 @@ public class MainStartVM {
private static StartVmDialog dialog;
public static final int PENDDING_EMPTY = -1;
public static final int STARTED_VM = 0;
public static final int STARTED_VM_READY_RUNNING = 1;
public static final int ERROR_VM = 10;
public static final int ERROR_BREAK = 11;
public static final int ERROR_INVALID_VM_ID = 12;
public static final int ERROR_MAKE_TEMP_FOLDER = 13;
public static final int ERROR_UNSAFE_COMMAND = 14;
public static final int ERROR_ACCEL = 15;
public static final int ERROR_VNC_TCP_PORT = 16;
public static final int ERROR_SHARED_FOLDER = 17;
public static final int ERROR_RESUME = 18;
public interface MainStartVMCallback {
void onStarted(int statusCode, String message);
void onError(int errorCode, String message);
}
public static void startNow(Activity activity, DataMainRoms vmConfig) {
breakNow = false;
@ -62,7 +81,10 @@ public class MainStartVM {
VMManager.setArch(vmConfig.itemArch, activity);
if (!(MainSettingsManager.getVmUi(activity).equals("X11") && DisplaySystem.isUseBuiltInX11())) {
if (
MainSettingsManager.getVmUi(activity).equals("VNC") ||
(MainSettingsManager.getVmUi(activity).equals("X11") && !DisplaySystem.isUseBuiltInX11())
) {
showDialog(activity, vmConfig.vmID, vmConfig.itemName, vmConfig.itemIcon, activity.getString(R.string.preparing));
}
@ -82,9 +104,22 @@ public class MainStartVM {
String thumbnailFile,
StartVmDialog dialog
) {
startNow(context, vmName, env, vmID, thumbnailFile, dialog, null);
}
public static void startNow(
Context context,
String vmName,
String env,
String vmID,
String thumbnailFile,
StartVmDialog dialog,
MainStartVMCallback callback
) {
if (breakNow) {
breakNow = false;
if (callback != null) callback.onError(PENDDING_EMPTY, "");
return;
}
@ -92,7 +127,10 @@ public class MainStartVM {
if (isLaunchFromPending) {
isLaunchFromPending = false;
if (pendingVMID.isEmpty()) return;
if (pendingVMID.isEmpty()) {
if (callback != null) callback.onError(ERROR_INVALID_VM_ID, "");
return;
}
pendingVMID = "";
} else {
if (MainSettingsManager.getVmUi(context).equals("X11") && !DisplaySystem.isUseBuiltInX11()) {
@ -128,8 +166,6 @@ public class MainStartVM {
breakNow = false;
if (dialog == null || !dialog.isShowing()) showDialog((Activity) context, vmID, vmName, thumbnailFile, null);
String finalvmID;
if (vmID == null || vmID.isEmpty()) {
finalvmID = VMManager.startRamdomVMID();
@ -140,14 +176,23 @@ public class MainStartVM {
Config.vmID = finalvmID;
if (VMManager.isVMRunning(context, finalvmID)) {
Toast.makeText(context, "This VM is already running.", Toast.LENGTH_LONG).show();
DisplaySystem.launch(context);
if (!MainSettingsManager.getVmUi(context).equals("VNC")) VmAudioManager.stream(vmID);
dismissDialog();
Toast.makeText(context, context.getString(R.string.this_vm_is_already_running), Toast.LENGTH_LONG).show();
DisplaySystem.launch(context);
if (
!MainSettingsManager.getVmUi(context).equals("VNC") &&
!DisplaySystem.isUseBuiltInX11()
)
VmAudioManager.stream(vmID);
if (callback != null) callback.onStarted(STARTED_VM_READY_RUNNING, "");
return;
}
// Place it here to avoid freezing when the dialog box appears upon returning to the virtual machine.
if (dialog == null || !dialog.isShowing()) showDialog((Activity) context, vmID, vmName, thumbnailFile, null);
File romDir = new File(Config.getCacheDir() + "/" + finalvmID);
if (!romDir.exists()) {
if (!romDir.mkdirs()) {
@ -159,6 +204,7 @@ public class MainStartVM {
);
dismissDialog();
if (callback != null) callback.onError(ERROR_MAKE_TEMP_FOLDER, "");
return;
}
}
@ -172,6 +218,7 @@ public class MainStartVM {
);
dismissDialog();
if (callback != null) callback.onError(ERROR_UNSAFE_COMMAND, "");
return;
}
@ -182,6 +229,7 @@ public class MainStartVM {
() -> startNow(context, vmName, env.replace("tcg,thread=multi", "tcg,thread=single"), finalvmID, thumbnailFile, finalDialog1), null, null);
dismissDialog();
if (callback != null) callback.onError(ERROR_ACCEL, "");
return;
}
}
@ -198,6 +246,7 @@ public class MainStartVM {
null);
dismissDialog();
if (callback != null) callback.onError(ERROR_VNC_TCP_PORT, "");
return;
}
@ -221,6 +270,7 @@ public class MainStartVM {
);
dismissDialog();
if (callback != null) callback.onError(ERROR_SHARED_FOLDER, "");
return;
}
@ -228,10 +278,11 @@ public class MainStartVM {
if (breakNow) {
dismissDialog();
if (callback != null) callback.onError(ERROR_BREAK, "");
return;
}
startVm(context, vmName, env, finalvmID, thumbnailFile);
startVm(context, vmName, env, finalvmID, thumbnailFile, callback);
});
}).start();
}
@ -241,7 +292,8 @@ public class MainStartVM {
String vmName,
String env,
String vmID,
String thumbnailFile
String thumbnailFile,
MainStartVMCallback callback
) {
VMManager.isQemuStopedWithError = false;
@ -257,6 +309,7 @@ public class MainStartVM {
if (breakNow) {
dismissDialog();
if (callback != null) callback.onError(ERROR_BREAK, "");
return;
}
@ -332,6 +385,8 @@ public class MainStartVM {
null,
null
);
if (callback != null) callback.onError(ERROR_RESUME, "");
});
return;
@ -372,12 +427,18 @@ public class MainStartVM {
Log.i(TAG, "Virtual machine running.");
if (activity != null) {
activity.runOnUiThread(MainStartVM::dismissDialog);
activity.runOnUiThread(() -> {
MainStartVM.dismissDialog();
if (callback != null) callback.onStarted(STARTED_VM, "");
});
}
}).start();
} else {
assert activity != null;
activity.runOnUiThread(MainStartVM::dismissDialog);
activity.runOnUiThread(() -> {
MainStartVM.dismissDialog();
if (callback != null) callback.onError(ERROR_VM, "");
});
}
skipIDEwithARM64DialogInStartVM = false;
@ -404,9 +465,9 @@ public class MainStartVM {
VMManager.isTryAgain = false;
}
public static void startPending(Context context) {
public static void startPending(Context context, MainStartVMCallback callback) {
isLaunchFromPending = true;
startNow(context, pendingVMName, pendingEnv, pendingVMID, pendingThumbnailFile, null);
startNow(context, pendingVMName, pendingEnv, pendingVMID, pendingThumbnailFile, null, callback);
}
private static void showDialog(Activity activity, String vmId, String vmName, String thumbnailPath, String status) {

View file

@ -3,18 +3,19 @@ package com.vectras.vm.x11;
import static android.Manifest.permission.WRITE_SECURE_SETTINGS;
import android.app.Dialog;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.os.Build.VERSION.SDK_INT;
import android.content.pm.PackageManager;
import android.os.Looper;
import static android.view.InputDevice.KEYBOARD_TYPE_ALPHABETIC;
import static android.view.KeyEvent.*;
import static android.view.WindowManager.LayoutParams.*;
import android.view.WindowManager;
import androidx.activity.OnBackPressedCallback;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.fragment.app.FragmentTransaction;
@ -28,6 +29,9 @@ import com.vectras.vm.databinding.DesktopControlsBinding;
import com.vectras.vm.databinding.GameControlsBinding;
import com.vectras.vm.main.core.MainStartVM;
import com.vectras.vm.manager.QmpSender;
import com.vectras.vm.manager.VmAudioManager;
import com.vectras.vm.manager.VmFileManager;
import com.vectras.vm.utils.StreamAudio;
import com.vectras.vm.utils.UIUtils;
import static com.vectras.vm.x11.CmdEntryPoint.ACTION_START;
@ -119,6 +123,8 @@ public class X11Activity extends AppCompatActivity implements View.OnApplyWindow
public boolean ctrlClicked = false;
public boolean altClicked = false;
private StreamAudio streamAudio;
ActivityX11Binding binding;
ControlsFragmentBinding bindingControls;
DesktopControlsBinding bindingDesktopControls;
@ -688,13 +694,30 @@ public class X11Activity extends AppCompatActivity implements View.OnApplyWindow
bindingDesktopControls.ctrlaltdelBtn.setVisibility(View.GONE);
MainStartVM.startPending(this);
MainStartVM.startPending(this, new MainStartVM.MainStartVMCallback() {
@Override
public void onStarted(int statusCode, String message) {
MainStartVM.dismissDialog();
setupSound();
}
@Override
public void onError(int errorCode, String message) {
MainStartVM.dismissDialog();
if (errorCode == MainStartVM.PENDDING_EMPTY) setupSound();
}
}
);
}
private void onBack() {
if (bindingControls.mainControl.getVisibility() == View.GONE) {
bindingControls.mainControl.setVisibility(View.VISIBLE);
} else {
if (streamAudio != null) {
if (!VmAudioManager.currentVmId.equals(Config.vmID)) streamAudio.setCross(null);
if (streamAudio.isPlaying()) streamAudio.stop();
}
finish();
}
}
@ -713,6 +736,7 @@ public class X11Activity extends AppCompatActivity implements View.OnApplyWindow
@Override
protected void onDestroy() {
unregisterReceiver(receiver);
if (streamAudio != null) streamAudio.stop();
super.onDestroy();
}
@ -790,7 +814,8 @@ public class X11Activity extends AppCompatActivity implements View.OnApplyWindow
v.startDragAndDrop(
ClipData.newPlainText("", ""),
new View.DragShadowBuilder(binding.buttonVisibility) {
public void onDrawShadow(@NonNull Canvas canvas) {}
public void onDrawShadow(@NonNull Canvas canvas) {
}
},
null,
View.DRAG_FLAG_GLOBAL);
@ -927,8 +952,7 @@ public class X11Activity extends AppCompatActivity implements View.OnApplyWindow
startTime = SystemClock.uptimeMillis();
binding.mouseButtonsPosition.setPressed(true);
break;
case MotionEvent.ACTION_MOVE:
{
case MotionEvent.ACTION_MOVE: {
int[] offset = new int[2];
int[] offset2 = new int[2];
binding.mouseButtons.getLocationOnScreen(offset);
@ -949,8 +973,7 @@ public class X11Activity extends AppCompatActivity implements View.OnApplyWindow
- binding.mouseButtons.getHeight()));
break;
}
case MotionEvent.ACTION_UP:
{
case MotionEvent.ACTION_UP: {
final int[] _pos = new int[2];
binding.mouseButtons.getLocationOnScreen(_pos);
int deltaX =
@ -1433,6 +1456,31 @@ public class X11Activity extends AppCompatActivity implements View.OnApplyWindow
return mInputHandler.shouldInterceptKeys();
}
private void setupSound() {
if (streamAudio == null) {
streamAudio = new StreamAudio(X11Activity.this);
streamAudio.setFile(VmFileManager.findAudioRaw(X11Activity.this, Config.vmID));
if (VmAudioManager.currentVmId.equals(Config.vmID) && VmAudioManager.streamAudio.isPlaying())
streamAudio.setCross(VmAudioManager.streamAudio);
playSound();
}
}
int playSoundRequests;
private void playSound() {
if (streamAudio == null || streamAudio.isPlaying() || playSoundRequests > 0) return;
playSoundRequests++;
streamAudio.stop();
new Handler(Looper.getMainLooper()).postDelayed(() -> {
if (!streamAudio.isPlaying()) streamAudio.play();
playSoundRequests--;
}, 100);
}
private void shutdownthisvm() {
QmpSender.quickShutdown();
Config.setDefault();

View file

@ -195,6 +195,25 @@
app:layout_constraintTop_toTopOf="parent"
android:clickable="false"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginTop="16dp" >
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="16dp"
android:src="@drawable/info_24px" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/x11_general_setting_note" />
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View file

@ -750,6 +750,7 @@
<string name="use_external_x11_note">Dùng ứng dụng Termux:X11 thay vì màn hình có sẵn.</string>
<string name="blur_effect">Hiệu ứng mờ</string>
<string name="blur_effect_note">Làm mờ xung quanh để tăng khả năng tập trung vào một vùng cần được chú ý.</string>
<string name="x11_general_setting_note">Bạn cần khởi động lại ứng dụng khi chuyển đổi giữa tích hợp sẵn và ngoài.</string>
<!-- CPUs -->

View file

@ -603,6 +603,7 @@
<string name="in_system_settings">In system settings.</string>
<string name="missing_packages">Missing packages</string>
<string name="missing_packages_content">These are the missing packages that you should install:</string>
<string name="x11_general_setting_note">You need to restart the app when switching between built-in and external.</string>
<!--======================TERMUX STRINGS====================-->

View file

@ -11,8 +11,9 @@ async function sendNotification() {
title: "Vectras VM",
message: "Welcome!",
image: "https://github.com/xoureldeen/Vectras-VM-Android/blob/master/resources/vectrasvm.png",
url : "https://github.com/xoureldeen/Vectras-VM-Android",
//targetVersions: "3.9.0,3.9.1"
url: "https://github.com/xoureldeen/Vectras-VM-Android",
//activityClass: "update",
//targetVersions: "4.1.0,4.2.0",
targetVersions: ""
},
topic: "vectrasvmandroidgithub"

View file

@ -44,7 +44,7 @@ rm -rf /usr/share/qemu
clear
echo -e "\e[1;37m[i] Downloading..."
aria2c -x 4 --async-dns=false --disable-ipv6 --check-certificate=false -o setup.tar.gz https://archive.org/download/qemu-7-2-22-for-vectras-vm-nbab/base-vectras-vm-arm64-v8a.tar.gz
aria2c -x 4 --async-dns=false --disable-ipv6 --check-certificate=false -o setup.tar.gz https://archive.org/download/qemu-7-2-22-for-vectras-vm-nbab/base-june-2026-vectras-vm-arm64-v8a.tar.gz
clear
echo -e "\e[1;37m[i] Installing..."

View file

@ -5,11 +5,11 @@
"url": "https://github.com/xoureldeen/Vectras-VM-Android/releases",
"Message": "<h2>4.1.0</h2>\nBugs fixed.",
"cancellable": true,
"versionCodeBeta":"127",
"versionNameBeta":"4.2.3",
"versionNameBetas":"4.0.1,4.0.2,4.0.3,4.0.4,4.0.5,4.0.6,4.0.7,4.0.8,4.0.9,4.1.1,4.1.2,4.1.3,4.1.4,4.1.5,4.1.6,4.1.7,4.1.8,4.1.9,4.2.0,4.2.1,4.2.2,4.2.3",
"versionCodeBeta":"128",
"versionNameBeta":"4.2.4",
"versionNameBetas":"4.0.1,4.0.2,4.0.3,4.0.4,4.0.5,4.0.6,4.0.7,4.0.8,4.0.9,4.1.1,4.1.2,4.1.3,4.1.4,4.1.5,4.1.6,4.1.7,4.1.8,4.1.9,4.2.0,4.2.1,4.2.2,4.2.3,4.2.4",
"sizeBeta": "45 MB",
"urlBeta": "https://github.com/AnBui2004/Vectras-VM-Emu-Android/releases",
"MessageBeta": "<h2>4.2.3</h2>Bugs fixed.",
"MessageBeta": "<h2>4.2.4</h2>Bugs fixed.",
"cancellableBeta": true
}